From 34570b914e5a9e22e319a644957c446b26d2b058 Mon Sep 17 00:00:00 2001 From: Tomer Rosenthal Date: Wed, 28 Jan 2026 16:01:41 -0800 Subject: [PATCH 1/7] Add Durable Task Python SDK documentation and setup instructions - Created SKILL.md for Durable Task Python SDK, detailing usage, patterns, and orchestration structure. - Added patterns.md with detailed implementation patterns for function chaining, fan-out/fan-in, human interaction, durable timers, and more. - Introduced setup.md for local development and Azure provisioning, including Docker setup, environment configuration, and deployment options. --- .../skills/durable-functions-dotnet/SKILL.md | 627 +++++++++++++ .../references/patterns.md | 888 ++++++++++++++++++ .../references/setup.md | 845 +++++++++++++++++ .claude/skills/durable-task-dotnet/SKILL.md | 215 +++++ .../references/patterns.md | 575 ++++++++++++ .../durable-task-dotnet/references/setup.md | 573 +++++++++++ .claude/skills/durable-task-java/SKILL.md | 482 ++++++++++ .../durable-task-java/references/patterns.md | 629 +++++++++++++ .../durable-task-java/references/setup.md | 837 +++++++++++++++++ .claude/skills/durable-task-python/SKILL.md | 484 ++++++++++ .../references/patterns.md | 416 ++++++++ .../durable-task-python/references/setup.md | 473 ++++++++++ 12 files changed, 7044 insertions(+) create mode 100644 .claude/skills/durable-functions-dotnet/SKILL.md create mode 100644 .claude/skills/durable-functions-dotnet/references/patterns.md create mode 100644 .claude/skills/durable-functions-dotnet/references/setup.md create mode 100644 .claude/skills/durable-task-dotnet/SKILL.md create mode 100644 .claude/skills/durable-task-dotnet/references/patterns.md create mode 100644 .claude/skills/durable-task-dotnet/references/setup.md create mode 100644 .claude/skills/durable-task-java/SKILL.md create mode 100644 .claude/skills/durable-task-java/references/patterns.md create mode 100644 .claude/skills/durable-task-java/references/setup.md create mode 100644 .claude/skills/durable-task-python/SKILL.md create mode 100644 .claude/skills/durable-task-python/references/patterns.md create mode 100644 .claude/skills/durable-task-python/references/setup.md diff --git a/.claude/skills/durable-functions-dotnet/SKILL.md b/.claude/skills/durable-functions-dotnet/SKILL.md new file mode 100644 index 0000000..587334d --- /dev/null +++ b/.claude/skills/durable-functions-dotnet/SKILL.md @@ -0,0 +1,627 @@ +--- +name: durable-functions-dotnet +description: Build durable, fault-tolerant workflows using Azure Durable Functions with .NET isolated worker and Durable Task Scheduler backend. Use when creating serverless orchestrations, activities, entities, or implementing patterns like function chaining, fan-out/fan-in, async HTTP APIs, human interaction, monitoring, or stateful aggregators. Applies to Azure Functions apps requiring durable execution, state persistence, or distributed coordination with built-in HTTP management APIs and Azure integration. +--- + +# Azure Durable Functions (.NET Isolated) with Durable Task Scheduler + +Build fault-tolerant, stateful serverless workflows using Azure Durable Functions connected to Azure Durable Task Scheduler. + +## Quick Start + +### Required NuGet Packages + +```xml + + + + + + + + + +``` + +### host.json Configuration (Durable Task Scheduler) + +```json +{ + "version": "2.0", + "extensions": { + "durableTask": { + "storageProvider": { + "type": "azureManaged", + "connectionStringName": "DTS_CONNECTION_STRING" + }, + "hubName": "%TASKHUB_NAME%" + } + } +} +``` + +### local.settings.json + +```json +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None", + "TASKHUB_NAME": "default" + } +} +``` + +### Minimal Example (Function-Based) + +```csharp +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using Microsoft.Extensions.Logging; + +public static class DurableFunctionsApp +{ + // HTTP Starter - triggers orchestration + [Function("HttpStart")] + public static async Task HttpStart( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "orchestrators/{functionName}")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + string functionName, + FunctionContext executionContext) + { + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(functionName); + + var logger = executionContext.GetLogger("HttpStart"); + logger.LogInformation("Started orchestration with ID = '{instanceId}'", instanceId); + + return await client.CreateCheckStatusResponseAsync(req, instanceId); + } + + // Orchestrator function + [Function(nameof(MyOrchestration))] + public static async Task MyOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) + { + ILogger logger = context.CreateReplaySafeLogger(nameof(MyOrchestration)); + logger.LogInformation("Starting orchestration"); + + var result1 = await context.CallActivityAsync(nameof(SayHello), "Tokyo"); + var result2 = await context.CallActivityAsync(nameof(SayHello), "Seattle"); + var result3 = await context.CallActivityAsync(nameof(SayHello), "London"); + + return $"{result1}, {result2}, {result3}"; + } + + // Activity function + [Function(nameof(SayHello))] + public static string SayHello([ActivityTrigger] string name, FunctionContext executionContext) + { + var logger = executionContext.GetLogger(nameof(SayHello)); + logger.LogInformation("Saying hello to {name}", name); + return $"Hello {name}!"; + } +} +``` + +### Program.cs Setup + +```csharp +using Microsoft.Extensions.Hosting; + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .Build(); + +await host.RunAsync(); +``` + +## Pattern Selection Guide + +| Pattern | Use When | +|---------|----------| +| **Function Chaining** | Sequential steps where each depends on the previous | +| **Fan-Out/Fan-In** | Parallel processing with aggregated results | +| **Async HTTP APIs** | Long-running operations with HTTP status polling | +| **Monitor** | Periodic polling with configurable timeouts | +| **Human Interaction** | Workflow pauses for external input/approval | +| **Aggregator (Entities)** | Stateful objects with operations (counters, accounts) | + +See [references/patterns.md](references/patterns.md) for detailed implementations. + +## Two Approaches: Function-Based vs Class-Based + +### Function-Based (Default) + +```csharp +[Function(nameof(MyOrchestration))] +public static async Task MyOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + string input = context.GetInput()!; + return await context.CallActivityAsync(nameof(MyActivity), input); +} + +[Function(nameof(MyActivity))] +public static string MyActivity([ActivityTrigger] string input) +{ + return $"Processed: {input}"; +} +``` + +### Class-Based (With Source Generator) + +Requires `Microsoft.DurableTask.Generators` package: + +```csharp +[DurableTask(nameof(MyOrchestration))] +public class MyOrchestration : TaskOrchestrator +{ + public override async Task RunAsync(TaskOrchestrationContext context, string input) + { + ILogger logger = context.CreateReplaySafeLogger(); + return await context.CallActivityAsync(nameof(MyActivity), input); + } +} + +[DurableTask(nameof(MyActivity))] +public class MyActivity : TaskActivity +{ + private readonly ILogger _logger; + + // Activities support DI - orchestrations do NOT + public MyActivity(ILogger logger) + { + _logger = logger; + } + + public override Task RunAsync(TaskActivityContext context, string input) + { + _logger.LogInformation("Processing: {Input}", input); + return Task.FromResult($"Processed: {input}"); + } +} +``` + +## Critical Rules + +### Orchestration Determinism + +Orchestrations replay from history - all code MUST be deterministic. When an orchestration resumes, it replays all previous code to rebuild state. Non-deterministic code produces different results on replay, causing `NonDeterministicOrchestrationException`. + +**NEVER do inside orchestrations:** +- `DateTime.Now`, `DateTime.UtcNow` → Use `context.CurrentUtcDateTime` +- `Guid.NewGuid()` → Use `context.NewGuid()` +- `Random` → Pass random values from activities +- Direct I/O, HTTP calls, database access → Move to activities +- `Thread.Sleep()`, `Task.Delay()` → Use `context.CreateTimer()` +- Non-deterministic LINQ (parallel, unordered) +- `Task.Run()`, `ConfigureAwait(false)` +- Static mutable variables +- Environment variables that may change → Pass as input or use activities + +**ALWAYS safe:** +- `context.CallActivityAsync()` +- `context.CallSubOrchestrationAsync()` +- `context.CallHttpAsync()` +- `context.CreateTimer()` +- `context.WaitForExternalEvent()` +- `context.CurrentUtcDateTime` +- `context.NewGuid()` +- `context.SetCustomStatus()` +- `context.CreateReplaySafeLogger()` + +### Non-Determinism Patterns (WRONG vs CORRECT) + +#### Getting Current Time + +```csharp +// WRONG - DateTime.UtcNow returns different value on replay +[Function(nameof(BadOrchestration))] +public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + DateTime currentTime = DateTime.UtcNow; // Non-deterministic! + if (currentTime.Hour < 12) + { + await context.CallActivityAsync(nameof(MorningActivity), null); + } +} + +// CORRECT - context.CurrentUtcDateTime replays consistently +[Function(nameof(GoodOrchestration))] +public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + DateTime currentTime = context.CurrentUtcDateTime; // Deterministic + if (currentTime.Hour < 12) + { + await context.CallActivityAsync(nameof(MorningActivity), null); + } +} +``` + +#### Generating GUIDs + +```csharp +// WRONG - Guid.NewGuid() generates different value on replay +[Function(nameof(BadOrchestration))] +public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + string orderId = Guid.NewGuid().ToString(); // Non-deterministic! + await context.CallActivityAsync(nameof(CreateOrder), orderId); + return orderId; +} + +// CORRECT - context.NewGuid() replays the same value +[Function(nameof(GoodOrchestration))] +public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + string orderId = context.NewGuid().ToString(); // Deterministic + await context.CallActivityAsync(nameof(CreateOrder), orderId); + return orderId; +} +``` + +#### Random Numbers + +```csharp +// WRONG - Random produces different values on replay +[Function(nameof(BadOrchestration))] +public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + int delay = new Random().Next(1, 10); // Non-deterministic! + await context.CreateTimer(context.CurrentUtcDateTime.AddSeconds(delay), CancellationToken.None); +} + +// CORRECT - generate random in activity, pass to orchestrator +[Function(nameof(GetRandomDelay))] +public static int GetRandomDelay([ActivityTrigger] object? input) +{ + return new Random().Next(1, 10); // OK in activity +} + +[Function(nameof(GoodOrchestration))] +public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + int delay = await context.CallActivityAsync(nameof(GetRandomDelay), null); + await context.CreateTimer(context.CurrentUtcDateTime.AddSeconds(delay), CancellationToken.None); +} +``` + +#### Sleeping/Delays + +```csharp +// WRONG - Thread.Sleep/Task.Delay don't persist and block +[Function(nameof(BadOrchestration))] +public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + await context.CallActivityAsync(nameof(Step1), null); + await Task.Delay(60000); // Non-durable! Lost on restart, wastes resources + await context.CallActivityAsync(nameof(Step2), null); +} + +// CORRECT - context.CreateTimer is durable +[Function(nameof(GoodOrchestration))] +public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + await context.CallActivityAsync(nameof(Step1), null); + await context.CreateTimer(context.CurrentUtcDateTime.AddMinutes(1), CancellationToken.None); // Durable + await context.CallActivityAsync(nameof(Step2), null); +} +``` + +#### HTTP Calls and I/O + +```csharp +// WRONG - HttpClient in orchestrator is non-deterministic +[Function(nameof(BadOrchestration))] +public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + using var client = new HttpClient(); + var response = await client.GetStringAsync("https://api.example.com/data"); // Non-deterministic! + return response; +} + +// CORRECT Option 1 - use CallHttpAsync (built-in durable HTTP) +[Function(nameof(GoodOrchestration1))] +public static async Task GoodOrchestration1([OrchestrationTrigger] TaskOrchestrationContext context) +{ + DurableHttpResponse response = await context.CallHttpAsync( + HttpMethod.Get, new Uri("https://api.example.com/data")); // Deterministic + return response.Content; +} + +// CORRECT Option 2 - move I/O to activity +[Function(nameof(FetchData))] +public static async Task FetchData([ActivityTrigger] string url) +{ + using var client = new HttpClient(); + return await client.GetStringAsync(url); // OK in activity +} + +[Function(nameof(GoodOrchestration2))] +public static async Task GoodOrchestration2([OrchestrationTrigger] TaskOrchestrationContext context) +{ + return await context.CallActivityAsync(nameof(FetchData), "https://api.example.com/data"); +} +``` + +#### Database Access + +```csharp +// WRONG - database query in orchestrator +[Function(nameof(BadOrchestration))] +public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + using var conn = new SqlConnection(connectionString); // Non-deterministic! + await conn.OpenAsync(); + // ... +} + +// CORRECT - database access in activity +[Function(nameof(GetUser))] +public static async Task GetUser([ActivityTrigger] string userId) +{ + using var conn = new SqlConnection(connectionString); // OK in activity + await conn.OpenAsync(); + // ... + return user; +} + +[Function(nameof(GoodOrchestration))] +public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + string userId = context.GetInput()!; + return await context.CallActivityAsync(nameof(GetUser), userId); +} +``` + +#### Environment Variables + +```csharp +// WRONG - env var might change between replays +[Function(nameof(BadOrchestration))] +public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + string apiEndpoint = Environment.GetEnvironmentVariable("API_ENDPOINT")!; // Could change! + await context.CallActivityAsync(nameof(CallApi), apiEndpoint); +} + +// CORRECT - pass config as input +[Function(nameof(GoodOrchestration))] +public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + var config = context.GetInput()!; + string apiEndpoint = config.ApiEndpoint; // From input, deterministic + await context.CallActivityAsync(nameof(CallApi), apiEndpoint); +} + +// ALSO CORRECT - read env var in activity +[Function(nameof(CallApi))] +public static async Task CallApi([ActivityTrigger] object? input) +{ + string apiEndpoint = Environment.GetEnvironmentVariable("API_ENDPOINT")!; // OK in activity + // make the call... +} +``` + +#### Collection Iteration Order + +```csharp +// WRONG - Dictionary iteration order may vary +[Function(nameof(BadOrchestration))] +public static async Task BadOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + var items = context.GetInput>()!; + foreach (var key in items.Keys) // Order not guaranteed! + { + await context.CallActivityAsync(nameof(Process), key); + } +} + +// CORRECT - use sorted keys for deterministic order +[Function(nameof(GoodOrchestration))] +public static async Task GoodOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + var items = context.GetInput>()!; + foreach (var key in items.Keys.OrderBy(k => k)) // Guaranteed order + { + await context.CallActivityAsync(nameof(Process), key); + } +} +``` + +### Logging in Orchestrations + +Use `CreateReplaySafeLogger` to avoid duplicate log entries during replay: + +```csharp +[Function(nameof(MyOrchestration))] +public static async Task MyOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + ILogger logger = context.CreateReplaySafeLogger(nameof(MyOrchestration)); + logger.LogInformation("Orchestration started"); // Only logs once, not on each replay + + var result = await context.CallActivityAsync(nameof(MyActivity), "input"); + + logger.LogInformation("Activity completed with result: {Result}", result); + return result; +} +``` + +### Error Handling + +```csharp +[Function(nameof(OrchestrationWithErrorHandling))] +public static async Task OrchestrationWithErrorHandling( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + string input = context.GetInput()!; + try + { + return await context.CallActivityAsync(nameof(RiskyActivity), input); + } + catch (TaskFailedException ex) + { + // Activity failed - implement compensation + context.SetCustomStatus(new { Error = ex.Message }); + return await context.CallActivityAsync(nameof(CompensationActivity), input); + } +} +``` + +### Retry Policies + +```csharp +var options = new TaskOptions +{ + Retry = new RetryPolicy( + maxNumberOfAttempts: 3, + firstRetryInterval: TimeSpan.FromSeconds(5), + backoffCoefficient: 2.0, + maxRetryInterval: TimeSpan.FromMinutes(1)) +}; + +await context.CallActivityAsync(nameof(UnreliableActivity), input, options); +``` + +## HTTP Management APIs + +Durable Functions exposes built-in HTTP APIs for orchestration management: + +### CreateCheckStatusResponse + +```csharp +[Function("HttpStart")] +public static async Task HttpStart( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "orchestrators/{functionName}")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + string functionName) +{ + // Parse input from request body + string? input = await new StreamReader(req.Body).ReadToEndAsync(); + + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(functionName, input); + + // Returns 202 Accepted with management URLs in response + return await client.CreateCheckStatusResponseAsync(req, instanceId); +} +``` + +Response includes: +- `statusQueryGetUri` - GET endpoint to check status +- `sendEventPostUri` - POST endpoint to raise events +- `terminatePostUri` - POST endpoint to terminate +- `purgeHistoryDeleteUri` - DELETE endpoint to purge history + +### Client Operations + +```csharp +[DurableClient] DurableTaskClient client + +// Schedule new orchestration +string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("MyOrchestration", input); + +// Schedule with custom instance ID +string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( + "MyOrchestration", input, new StartOrchestrationOptions { InstanceId = "my-custom-id" }); + +// Get status +OrchestrationMetadata? state = await client.GetInstanceAsync(instanceId, getInputsAndOutputs: true); + +// Wait for completion +OrchestrationMetadata? result = await client.WaitForInstanceCompletionAsync( + instanceId, getInputsAndOutputs: true, cancellationToken); + +// Raise external event +await client.RaiseEventAsync(instanceId, "ApprovalEvent", approvalData); + +// Terminate +await client.TerminateInstanceAsync(instanceId, "User cancelled"); + +// Suspend/Resume +await client.SuspendInstanceAsync(instanceId, "Pausing for maintenance"); +await client.ResumeInstanceAsync(instanceId, "Resuming operation"); + +// Purge history +await client.PurgeInstanceAsync(instanceId); +``` + +## Connection & Authentication + +### Connection String Formats + +```csharp +// Local emulator (no auth) +"Endpoint=http://localhost:8080;Authentication=None" + +// Azure with Managed Identity (recommended for production) +"Endpoint=https://my-scheduler.region.durabletask.io;Authentication=ManagedIdentity" + +// Azure with specific client ID (user-assigned managed identity) +"Endpoint=https://my-scheduler.region.durabletask.io;Authentication=ManagedIdentity;ClientId=" +``` + +Note: Durable Task Scheduler supports identity-based authentication only - no connection strings with keys. + +## Local Development with Emulator + +```bash +# Start Azurite (required for Azure Functions) +azurite start + +# Pull and run the Durable Task Scheduler emulator +docker pull mcr.microsoft.com/dts/dts-emulator:latest +docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest + +# Dashboard available at http://localhost:8082 + +# Start the function app +func start +``` + +## Durable HTTP Calls + +Make HTTP calls directly from orchestrations (persisted and replay-safe): + +```csharp +[Function(nameof(CallExternalApi))] +public static async Task CallExternalApi([OrchestrationTrigger] TaskOrchestrationContext context) +{ + // Simple GET + DurableHttpResponse response = await context.CallHttpAsync(HttpMethod.Get, new Uri("https://api.example.com/data")); + + if (response.StatusCode != HttpStatusCode.OK) + { + throw new Exception($"API call failed: {response.StatusCode}"); + } + + return response.Content; +} + +// With headers and body +var request = new DurableHttpRequest( + HttpMethod.Post, + new Uri("https://api.example.com/data")) +{ + Headers = { ["Content-Type"] = "application/json" }, + Content = JsonSerializer.Serialize(payload) +}; + +DurableHttpResponse response = await context.CallHttpAsync(request); + +// With managed identity authentication +var request = new DurableHttpRequest( + HttpMethod.Get, + new Uri("https://management.azure.com/...")) +{ + TokenSource = new ManagedIdentityTokenSource("https://management.azure.com/.default") +}; +``` + +## References + +- **[patterns.md](references/patterns.md)** - Detailed pattern implementations (Fan-Out/Fan-In, Human Interaction, Entities, Sub-Orchestrations, Monitor) +- **[setup.md](references/setup.md)** - Azure Durable Task Scheduler provisioning, deployment, and project templates diff --git a/.claude/skills/durable-functions-dotnet/references/patterns.md b/.claude/skills/durable-functions-dotnet/references/patterns.md new file mode 100644 index 0000000..697bcf9 --- /dev/null +++ b/.claude/skills/durable-functions-dotnet/references/patterns.md @@ -0,0 +1,888 @@ +# Durable Functions Patterns Reference + +Complete implementations for common Durable Functions workflow patterns. + +## Function Chaining + +Sequential workflow where each step depends on the previous result: + +```csharp +[Function(nameof(ProcessOrderOrchestration))] +public static async Task ProcessOrderOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + var order = context.GetInput()!; + ILogger logger = context.CreateReplaySafeLogger(nameof(ProcessOrderOrchestration)); + + logger.LogInformation("Processing order {OrderId}", order.Id); + + // Step 1: Validate order + var validation = await context.CallActivityAsync(nameof(ValidateOrder), order); + if (!validation.IsValid) + { + return new OrderResult { Success = false, Error = validation.Error }; + } + + // Step 2: Reserve inventory + var reservation = await context.CallActivityAsync(nameof(ReserveInventory), order); + + // Step 3: Process payment + var payment = await context.CallActivityAsync(nameof(ProcessPayment), + new PaymentRequest { Order = order, ReservationId = reservation.Id }); + + // Step 4: Ship order + var shipment = await context.CallActivityAsync(nameof(ShipOrder), + new ShipmentRequest { Order = order, PaymentId = payment.TransactionId }); + + // Step 5: Notify customer + await context.CallActivityAsync(nameof(NotifyCustomer), + new Notification { OrderId = order.Id, TrackingNumber = shipment.TrackingNumber }); + + return new OrderResult { Success = true, TrackingNumber = shipment.TrackingNumber }; +} + +[Function(nameof(ValidateOrder))] +public static ValidationResult ValidateOrder([ActivityTrigger] Order order) => /* ... */; + +[Function(nameof(ReserveInventory))] +public static async Task ReserveInventory([ActivityTrigger] Order order) => /* ... */; + +[Function(nameof(ProcessPayment))] +public static async Task ProcessPayment([ActivityTrigger] PaymentRequest request) => /* ... */; + +[Function(nameof(ShipOrder))] +public static async Task ShipOrder([ActivityTrigger] ShipmentRequest request) => /* ... */; + +[Function(nameof(NotifyCustomer))] +public static async Task NotifyCustomer([ActivityTrigger] Notification notification) => /* ... */; +``` + +## Fan-Out/Fan-In + +Process items in parallel and aggregate results: + +```csharp +[Function(nameof(ParallelProcessingOrchestration))] +public static async Task ParallelProcessingOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + var items = context.GetInput>()!; + ILogger logger = context.CreateReplaySafeLogger(nameof(ParallelProcessingOrchestration)); + + logger.LogInformation("Processing {Count} items in parallel", items.Count); + + // Fan-out: Create tasks for all items + var tasks = items.Select(item => + context.CallActivityAsync(nameof(ProcessItem), item)); + + // Fan-in: Wait for all to complete + ItemResult[] results = await Task.WhenAll(tasks); + + // Aggregate results + return new BatchResult + { + TotalProcessed = results.Length, + Successful = results.Count(r => r.Success), + Failed = results.Count(r => !r.Success) + }; +} + +[Function(nameof(ProcessItem))] +public static async Task ProcessItem([ActivityTrigger] WorkItem item) +{ + try + { + // Process the item + return new ItemResult { ItemId = item.Id, Success = true }; + } + catch (Exception ex) + { + return new ItemResult { ItemId = item.Id, Success = false, Error = ex.Message }; + } +} +``` + +### Fan-Out with Batching (Large Scale) + +For very large workloads, process in batches to avoid memory issues: + +```csharp +[Function(nameof(BatchedFanOutOrchestration))] +public static async Task BatchedFanOutOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + var input = context.GetInput()!; + ILogger logger = context.CreateReplaySafeLogger(nameof(BatchedFanOutOrchestration)); + + const int batchSize = 100; + var allResults = new List(); + + // Process in batches + for (int i = 0; i < input.Items.Count; i += batchSize) + { + var batch = input.Items.Skip(i).Take(batchSize).ToList(); + logger.LogInformation("Processing batch {BatchNumber} ({Count} items)", + i / batchSize + 1, batch.Count); + + var batchTasks = batch.Select(item => + context.CallActivityAsync(nameof(ProcessItem), item)); + + var batchResults = await Task.WhenAll(batchTasks); + allResults.AddRange(batchResults); + } + + return new BatchResult + { + TotalProcessed = allResults.Count, + Successful = allResults.Count(r => r.Success), + Failed = allResults.Count(r => !r.Success) + }; +} +``` + +### Fan-Out with Partial Failure Handling + +```csharp +[Function(nameof(ResilientFanOutOrchestration))] +public static async Task ResilientFanOutOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + var items = context.GetInput>()!; + var results = new List(); + var errors = new List(); + + // Create tasks for all items + var taskItemPairs = items.Select(item => new + { + Task = context.CallActivityAsync(nameof(ProcessItem), item), + Item = item + }).ToList(); + + // Wait for all tasks, catching individual failures + foreach (var pair in taskItemPairs) + { + try + { + var result = await pair.Task; + results.Add(result); + } + catch (TaskFailedException ex) + { + errors.Add($"Item {pair.Item.Id} failed: {ex.Message}"); + results.Add(new ItemResult { ItemId = pair.Item.Id, Success = false, Error = ex.Message }); + } + } + + return new ProcessingResult + { + Results = results, + Errors = errors, + AllSuccessful = errors.Count == 0 + }; +} +``` + +## Human Interaction (Approval Workflow) + +Wait for external input with timeout: + +```csharp +[Function(nameof(ApprovalOrchestration))] +public static async Task ApprovalOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + var request = context.GetInput()!; + ILogger logger = context.CreateReplaySafeLogger(nameof(ApprovalOrchestration)); + + // Notify approver + await context.CallActivityAsync(nameof(SendApprovalRequest), new ApprovalNotification + { + RequestId = context.InstanceId, + Requester = request.Requester, + Amount = request.Amount, + Approver = request.Approver + }); + + logger.LogInformation("Waiting for approval from {Approver}", request.Approver); + + // Wait for approval event with 72-hour timeout + using var cts = new CancellationTokenSource(); + var approvalTask = context.WaitForExternalEvent("ApprovalResponse"); + var timeoutTask = context.CreateTimer(context.CurrentUtcDateTime.AddHours(72), cts.Token); + + var winner = await Task.WhenAny(approvalTask, timeoutTask); + + if (winner == approvalTask) + { + cts.Cancel(); // Cancel the timer + var response = await approvalTask; + + if (response.Approved) + { + await context.CallActivityAsync(nameof(ExecuteApprovedAction), request); + return new ApprovalResult { Status = "Approved", ApprovedBy = response.ApproverName }; + } + else + { + return new ApprovalResult { Status = "Rejected", Reason = response.RejectionReason }; + } + } + else + { + // Timeout - escalate + logger.LogWarning("Approval timed out, escalating"); + await context.CallActivityAsync(nameof(EscalateApproval), request); + return new ApprovalResult { Status = "Escalated", Reason = "Approval timeout" }; + } +} + +// HTTP endpoint to submit approval +[Function("SubmitApproval")] +public static async Task SubmitApproval( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "approval/{instanceId}")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + string instanceId) +{ + var response = await req.ReadFromJsonAsync(); + + await client.RaiseEventAsync(instanceId, "ApprovalResponse", response); + + var httpResponse = req.CreateResponse(HttpStatusCode.Accepted); + await httpResponse.WriteAsJsonAsync(new { Message = "Approval submitted" }); + return httpResponse; +} + +[Function(nameof(SendApprovalRequest))] +public static async Task SendApprovalRequest([ActivityTrigger] ApprovalNotification notification) +{ + // Send email/notification to approver +} + +[Function(nameof(ExecuteApprovedAction))] +public static async Task ExecuteApprovedAction([ActivityTrigger] ApprovalRequest request) +{ + // Execute the approved action +} + +[Function(nameof(EscalateApproval))] +public static async Task EscalateApproval([ActivityTrigger] ApprovalRequest request) +{ + // Escalate to manager +} +``` + +### Multi-Level Approval + +```csharp +[Function(nameof(MultiLevelApprovalOrchestration))] +public static async Task MultiLevelApprovalOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + var request = context.GetInput()!; + + foreach (var approver in request.ApprovalChain) + { + // Request approval at this level + await context.CallActivityAsync(nameof(SendApprovalRequest), new ApprovalNotification + { + RequestId = context.InstanceId, + Approver = approver.Email, + Level = approver.Level + }); + + // Wait for this level's approval + using var cts = new CancellationTokenSource(); + var approvalTask = context.WaitForExternalEvent($"Approval_{approver.Level}"); + var timeoutTask = context.CreateTimer(context.CurrentUtcDateTime.AddHours(24), cts.Token); + + var winner = await Task.WhenAny(approvalTask, timeoutTask); + + if (winner == timeoutTask) + { + return new ApprovalResult { Status = "TimedOut", Level = approver.Level }; + } + + cts.Cancel(); + var response = await approvalTask; + + if (!response.Approved) + { + return new ApprovalResult + { + Status = "Rejected", + Level = approver.Level, + Reason = response.RejectionReason + }; + } + } + + // All levels approved + await context.CallActivityAsync(nameof(ExecuteApprovedAction), request); + return new ApprovalResult { Status = "FullyApproved" }; +} +``` + +## Monitor Pattern + +Periodic polling with configurable timeout and exponential backoff: + +```csharp +[Function(nameof(ResourceMonitorOrchestration))] +public static async Task ResourceMonitorOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + var config = context.GetInput()!; + ILogger logger = context.CreateReplaySafeLogger(nameof(ResourceMonitorOrchestration)); + + DateTime deadline = context.CurrentUtcDateTime.Add(config.MaxDuration); + TimeSpan pollingInterval = config.InitialPollingInterval; + int checkCount = 0; + + while (context.CurrentUtcDateTime < deadline) + { + checkCount++; + logger.LogInformation("Check #{Count}: Polling resource {ResourceId}", checkCount, config.ResourceId); + + var status = await context.CallActivityAsync( + nameof(CheckResourceStatus), config.ResourceId); + + if (status.IsReady) + { + logger.LogInformation("Resource {ResourceId} is ready after {Count} checks", + config.ResourceId, checkCount); + return new MonitorResult + { + Success = true, + Status = status, + CheckCount = checkCount + }; + } + + if (status.IsFailed) + { + logger.LogError("Resource {ResourceId} failed", config.ResourceId); + return new MonitorResult + { + Success = false, + Error = "Resource provisioning failed", + Status = status + }; + } + + // Wait before next check (exponential backoff) + var nextCheck = context.CurrentUtcDateTime.Add(pollingInterval); + if (nextCheck >= deadline) + { + break; // Don't wait if we'll exceed deadline + } + + await context.CreateTimer(nextCheck, CancellationToken.None); + + // Exponential backoff with cap + pollingInterval = TimeSpan.FromSeconds( + Math.Min(pollingInterval.TotalSeconds * config.BackoffMultiplier, + config.MaxPollingInterval.TotalSeconds)); + } + + // Timeout + return new MonitorResult + { + Success = false, + Error = "Monitoring timeout exceeded", + CheckCount = checkCount + }; +} + +[Function(nameof(CheckResourceStatus))] +public static async Task CheckResourceStatus([ActivityTrigger] string resourceId) +{ + // Check resource status via API + return new ResourceStatus { /* ... */ }; +} + +public record MonitorConfig +{ + public string ResourceId { get; init; } = ""; + public TimeSpan MaxDuration { get; init; } = TimeSpan.FromHours(2); + public TimeSpan InitialPollingInterval { get; init; } = TimeSpan.FromSeconds(10); + public TimeSpan MaxPollingInterval { get; init; } = TimeSpan.FromMinutes(5); + public double BackoffMultiplier { get; init; } = 1.5; +} +``` + +## Durable Entities (Aggregator Pattern) + +Stateful objects that maintain state across operations: + +### Counter Entity + +```csharp +// Entity function +[Function(nameof(Counter))] +public static Task Counter([EntityTrigger] TaskEntityDispatcher dispatcher) + => dispatcher.DispatchAsync(); + +// Entity class +public class CounterEntity +{ + public int Value { get; set; } + + public void Add(int amount) => Value += amount; + public void Subtract(int amount) => Value -= amount; + public void Reset() => Value = 0; + public int Get() => Value; +} + +// Using entity from orchestration +[Function(nameof(CounterOrchestration))] +public static async Task CounterOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + var entityId = new EntityInstanceId(nameof(Counter), "myCounter"); + + // Signal (fire-and-forget) + await context.Entities.SignalEntityAsync(entityId, "Add", 5); + await context.Entities.SignalEntityAsync(entityId, "Add", 10); + + // Call and get result + int value = await context.Entities.CallEntityAsync(entityId, "Get"); + + return value; // Returns 15 +} + +// HTTP endpoint to interact with entity +[Function("CounterOperation")] +public static async Task CounterOperation( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "counter/{entityKey}/{operation}")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + string entityKey, + string operation) +{ + var entityId = new EntityInstanceId(nameof(Counter), entityKey); + var amount = await req.ReadFromJsonAsync(); + + // Signal the entity + await client.Entities.SignalEntityAsync(entityId, operation, amount); + + var response = req.CreateResponse(HttpStatusCode.Accepted); + return response; +} + +[Function("GetCounter")] +public static async Task GetCounter( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "counter/{entityKey}")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + string entityKey) +{ + var entityId = new EntityInstanceId(nameof(Counter), entityKey); + var state = await client.Entities.GetEntityAsync(entityId); + + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(state?.State ?? new CounterEntity()); + return response; +} +``` + +### Account Entity with Locking + +```csharp +[Function(nameof(BankAccount))] +public static Task BankAccount([EntityTrigger] TaskEntityDispatcher dispatcher) + => dispatcher.DispatchAsync(); + +public class BankAccountEntity +{ + public decimal Balance { get; set; } + public List History { get; set; } = new(); + + public bool Deposit(decimal amount) + { + if (amount <= 0) return false; + + Balance += amount; + History.Add(new Transaction { Type = "Deposit", Amount = amount, Timestamp = DateTime.UtcNow }); + return true; + } + + public bool Withdraw(decimal amount) + { + if (amount <= 0 || Balance < amount) return false; + + Balance -= amount; + History.Add(new Transaction { Type = "Withdrawal", Amount = amount, Timestamp = DateTime.UtcNow }); + return true; + } + + public decimal GetBalance() => Balance; + public List GetHistory() => History; +} + +// Transfer between accounts (uses locking) +[Function(nameof(TransferOrchestration))] +public static async Task TransferOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + var request = context.GetInput()!; + + var fromEntity = new EntityInstanceId(nameof(BankAccount), request.FromAccount); + var toEntity = new EntityInstanceId(nameof(BankAccount), request.ToAccount); + + // Lock both accounts in a consistent order to prevent deadlocks + var entities = new[] { fromEntity.ToString(), toEntity.ToString() }.OrderBy(x => x).ToArray(); + + using (await context.Entities.LockEntitiesAsync( + entities.Select(e => EntityInstanceId.Parse(e)).ToArray())) + { + // Check balance + decimal balance = await context.Entities.CallEntityAsync(fromEntity, "GetBalance"); + if (balance < request.Amount) + { + return new TransferResult { Success = false, Error = "Insufficient funds" }; + } + + // Perform transfer + await context.Entities.CallEntityAsync(fromEntity, "Withdraw", request.Amount); + await context.Entities.CallEntityAsync(toEntity, "Deposit", request.Amount); + + return new TransferResult { Success = true }; + } +} +``` + +## Sub-Orchestrations + +Compose workflows by calling other orchestrations: + +```csharp +[Function(nameof(OrderFulfillmentOrchestration))] +public static async Task OrderFulfillmentOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + var order = context.GetInput()!; + ILogger logger = context.CreateReplaySafeLogger(nameof(OrderFulfillmentOrchestration)); + + logger.LogInformation("Fulfilling order {OrderId}", order.Id); + + // Call sub-orchestration for payment processing + var paymentResult = await context.CallSubOrchestrationAsync( + nameof(PaymentOrchestration), + new PaymentRequest { OrderId = order.Id, Amount = order.Total }); + + if (!paymentResult.Success) + { + return new FulfillmentResult { Success = false, Error = "Payment failed" }; + } + + // Call sub-orchestration for shipping (with custom instance ID) + var shipmentResult = await context.CallSubOrchestrationAsync( + nameof(ShippingOrchestration), + new ShipmentRequest { OrderId = order.Id, Items = order.Items }, + new SubOrchestrationOptions { InstanceId = $"ship-{order.Id}" }); + + return new FulfillmentResult + { + Success = true, + PaymentId = paymentResult.TransactionId, + TrackingNumber = shipmentResult.TrackingNumber + }; +} + +[Function(nameof(PaymentOrchestration))] +public static async Task PaymentOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + var request = context.GetInput()!; + + var authResult = await context.CallActivityAsync(nameof(AuthorizePayment), request); + if (!authResult.Authorized) + { + return new PaymentResult { Success = false, Error = authResult.DeclineReason }; + } + + var captureResult = await context.CallActivityAsync(nameof(CapturePayment), authResult.AuthorizationId); + + return new PaymentResult { Success = true, TransactionId = captureResult.TransactionId }; +} + +[Function(nameof(ShippingOrchestration))] +public static async Task ShippingOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + var request = context.GetInput()!; + + var label = await context.CallActivityAsync(nameof(CreateShippingLabel), request); + await context.CallActivityAsync(nameof(NotifyWarehouse), label); + + return new ShipmentResult { TrackingNumber = label.TrackingNumber }; +} +``` + +## Saga Pattern (Distributed Transactions) + +Implement compensating transactions for handling partial failures: + +```csharp +[Function(nameof(BookTravelOrchestration))] +public static async Task BookTravelOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + var request = context.GetInput()!; + ILogger logger = context.CreateReplaySafeLogger(nameof(BookTravelOrchestration)); + + var completedSteps = new Stack(); + + try + { + // Step 1: Book flight + var flight = await context.CallActivityAsync(nameof(BookFlight), request.Flight); + completedSteps.Push("flight"); + logger.LogInformation("Flight booked: {ConfirmationNumber}", flight.ConfirmationNumber); + + // Step 2: Book hotel + var hotel = await context.CallActivityAsync(nameof(BookHotel), request.Hotel); + completedSteps.Push("hotel"); + logger.LogInformation("Hotel booked: {ConfirmationNumber}", hotel.ConfirmationNumber); + + // Step 3: Book car + var car = await context.CallActivityAsync(nameof(BookCar), request.Car); + completedSteps.Push("car"); + logger.LogInformation("Car booked: {ConfirmationNumber}", car.ConfirmationNumber); + + return new TravelBookingResult + { + Success = true, + FlightConfirmation = flight.ConfirmationNumber, + HotelConfirmation = hotel.ConfirmationNumber, + CarConfirmation = car.ConfirmationNumber + }; + } + catch (TaskFailedException ex) + { + logger.LogError(ex, "Booking failed, initiating compensation"); + + // Compensate in reverse order + var compensationErrors = new List(); + + while (completedSteps.Count > 0) + { + var step = completedSteps.Pop(); + try + { + switch (step) + { + case "car": + await context.CallActivityAsync(nameof(CancelCar), request.Car); + break; + case "hotel": + await context.CallActivityAsync(nameof(CancelHotel), request.Hotel); + break; + case "flight": + await context.CallActivityAsync(nameof(CancelFlight), request.Flight); + break; + } + logger.LogInformation("Compensated: {Step}", step); + } + catch (TaskFailedException compEx) + { + compensationErrors.Add($"Failed to compensate {step}: {compEx.Message}"); + } + } + + return new TravelBookingResult + { + Success = false, + Error = ex.Message, + CompensationErrors = compensationErrors + }; + } +} + +// Activity functions +[Function(nameof(BookFlight))] +public static async Task BookFlight([ActivityTrigger] FlightRequest request) => /* ... */; + +[Function(nameof(BookHotel))] +public static async Task BookHotel([ActivityTrigger] HotelRequest request) => /* ... */; + +[Function(nameof(BookCar))] +public static async Task BookCar([ActivityTrigger] CarRequest request) => /* ... */; + +[Function(nameof(CancelFlight))] +public static async Task CancelFlight([ActivityTrigger] FlightRequest request) => /* ... */; + +[Function(nameof(CancelHotel))] +public static async Task CancelHotel([ActivityTrigger] HotelRequest request) => /* ... */; + +[Function(nameof(CancelCar))] +public static async Task CancelCar([ActivityTrigger] CarRequest request) => /* ... */; +``` + +## Eternal Orchestration (Continue-As-New) + +Long-running workflows that periodically restart to manage history size: + +```csharp +[Function(nameof(EternalProcessorOrchestration))] +public static async Task EternalProcessorOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + var state = context.GetInput() ?? new ProcessorState(); + ILogger logger = context.CreateReplaySafeLogger(nameof(EternalProcessorOrchestration)); + + // Check for external stop signal + bool stopRequested = false; + try + { + stopRequested = await context.WaitForExternalEvent( + "StopRequested", + TimeSpan.Zero); // Non-blocking check + } + catch (TaskCanceledException) + { + stopRequested = false; + } + + if (stopRequested) + { + logger.LogInformation("Stop requested, exiting eternal orchestration"); + return; + } + + // Do work + logger.LogInformation("Processing iteration {Iteration}", state.IterationCount); + + var newItems = await context.CallActivityAsync>(nameof(GetNewItems), state.LastProcessedId); + + if (newItems.Any()) + { + var tasks = newItems.Select(item => + context.CallActivityAsync(nameof(ProcessItem), item)); + await Task.WhenAll(tasks); + + state.LastProcessedId = newItems.Max(i => i.Id); + state.TotalProcessed += newItems.Count; + } + + state.IterationCount++; + + // Wait before next iteration + await context.CreateTimer(context.CurrentUtcDateTime.AddMinutes(1), CancellationToken.None); + + // Continue-as-new to prevent unbounded history growth + context.ContinueAsNew(state); +} + +public class ProcessorState +{ + public int IterationCount { get; set; } + public long LastProcessedId { get; set; } + public long TotalProcessed { get; set; } +} + +// HTTP endpoint to stop the eternal orchestration +[Function("StopEternalOrchestration")] +public static async Task StopEternalOrchestration( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "eternal/{instanceId}/stop")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + string instanceId) +{ + await client.RaiseEventAsync(instanceId, "StopRequested", true); + return req.CreateResponse(HttpStatusCode.Accepted); +} +``` + +## Scheduled/Timer-Based Workflows + +### Delayed Execution + +```csharp +[Function(nameof(ScheduledReminderOrchestration))] +public static async Task ScheduledReminderOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + var reminder = context.GetInput()!; + + // Wait until scheduled time + await context.CreateTimer(reminder.ScheduledTime, CancellationToken.None); + + // Send the reminder + await context.CallActivityAsync(nameof(SendReminder), reminder); +} +``` + +### Recurring Execution with Cancellation + +```csharp +[Function(nameof(RecurringJobOrchestration))] +public static async Task RecurringJobOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + var config = context.GetInput()!; + ILogger logger = context.CreateReplaySafeLogger(nameof(RecurringJobOrchestration)); + + DateTime endTime = context.CurrentUtcDateTime.Add(config.TotalDuration); + + while (context.CurrentUtcDateTime < endTime) + { + logger.LogInformation("Executing scheduled job"); + + // Execute job + await context.CallActivityAsync(nameof(ExecuteJob), config.JobParameters); + + // Wait for cancel OR next interval + using var cts = new CancellationTokenSource(); + var cancelTask = context.WaitForExternalEvent("Cancel"); + var timerTask = context.CreateTimer( + context.CurrentUtcDateTime.Add(config.Interval), + cts.Token); + + var winner = await Task.WhenAny(cancelTask, timerTask); + if (winner == cancelTask && await cancelTask) + { + logger.LogInformation("Job cancelled"); + return; + } + + cts.Cancel(); + } + + logger.LogInformation("Recurring job completed"); +} +``` + +## Version-Aware Orchestrations + +Handle orchestration versioning with running instances: + +```csharp +[Function(nameof(VersionedOrchestration))] +public static async Task VersionedOrchestration([OrchestrationTrigger] TaskOrchestrationContext context) +{ + var input = context.GetInput()!; + const int CurrentVersion = 2; + + // Handle different versions + if (input.Version < CurrentVersion) + { + // Legacy behavior for in-flight instances + return await LegacyWorkflow(context, input); + } + + // Current version behavior + return await CurrentWorkflow(context, input); +} + +private static async Task LegacyWorkflow(TaskOrchestrationContext context, VersionedInput input) +{ + // Original implementation + var result = await context.CallActivityAsync(nameof(OldActivity), input.Data); + return result; +} + +private static async Task CurrentWorkflow(TaskOrchestrationContext context, VersionedInput input) +{ + // New improved implementation + var step1 = await context.CallActivityAsync(nameof(NewActivityStep1), input.Data); + var step2 = await context.CallActivityAsync(nameof(NewActivityStep2), step1); + return step2; +} + +public record VersionedInput +{ + public int Version { get; init; } = 2; // Default to current version + public string Data { get; init; } = ""; +} +``` diff --git a/.claude/skills/durable-functions-dotnet/references/setup.md b/.claude/skills/durable-functions-dotnet/references/setup.md new file mode 100644 index 0000000..d244968 --- /dev/null +++ b/.claude/skills/durable-functions-dotnet/references/setup.md @@ -0,0 +1,845 @@ +# Durable Functions Setup Reference + +Complete setup and deployment guidance for Azure Durable Functions with Durable Task Scheduler. + +## Local Development + +### Prerequisites + +```bash +# Install Azure Functions Core Tools +brew tap azure/functions +brew install azure-functions-core-tools@4 + +# Install .NET SDK +brew install dotnet + +# Install Azure CLI (optional, for Azure deployment) +brew install azure-cli + +# Install Azurite (Azure Storage emulator) +npm install -g azurite +``` + +### Start Local Emulator + +```bash +# Terminal 1: Start Azurite (required for Azure Functions) +azurite start + +# Terminal 2: Start Durable Task Scheduler emulator +docker pull mcr.microsoft.com/dts/dts-emulator:latest +docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest + +# Dashboard available at http://localhost:8082 +``` + +### Docker Compose (All-in-One) + +```yaml +# docker-compose.yml +version: '3.8' + +services: + azurite: + image: mcr.microsoft.com/azure-storage/azurite:latest + ports: + - "10000:10000" # Blob + - "10001:10001" # Queue + - "10002:10002" # Table + volumes: + - azurite-data:/data + command: azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 + + dts-emulator: + image: mcr.microsoft.com/dts/dts-emulator:latest + ports: + - "8080:8080" # gRPC/HTTP endpoint + - "8082:8082" # Dashboard + environment: + - DTS_EMULATOR_LOG_LEVEL=Information + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8082/health"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + azurite-data: +``` + +```bash +docker-compose up -d +``` + +## Project Setup + +### Create New Project + +```bash +# Create Functions project +func init MyDurableFunctions --worker-runtime dotnet-isolated --target-framework net8.0 + +cd MyDurableFunctions + +# Add required packages +dotnet add package Microsoft.Azure.Functions.Worker.Extensions.DurableTask +dotnet add package Azure.Identity + +# Optional: class-based orchestrations +dotnet add package Microsoft.DurableTask.Generators +``` + +### Complete .csproj Template + +```xml + + + net8.0 + v4 + Exe + enable + enable + MyDurableFunctions + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + +``` + +### host.json (Durable Task Scheduler) + +```json +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true + } + }, + "extensions": { + "durableTask": { + "storageProvider": { + "type": "azureManaged", + "connectionStringName": "DTS_CONNECTION_STRING" + }, + "hubName": "%TASKHUB_NAME%" + } + } +} +``` + +### local.settings.json + +```json +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "DTS_CONNECTION_STRING": "Endpoint=http://localhost:8080;Authentication=None", + "TASKHUB_NAME": "default" + } +} +``` + +### Program.cs with DI and Logging + +```csharp +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureServices(services => + { + // Add Application Insights + services.AddApplicationInsightsTelemetryWorkerService(); + services.ConfigureFunctionsApplicationInsights(); + + // Add your services + services.AddHttpClient(); + services.AddSingleton(); + }) + .ConfigureLogging(logging => + { + logging.SetMinimumLevel(LogLevel.Information); + }) + .Build(); + +await host.RunAsync(); +``` + +## Complete Application Template + +### Functions.cs + +```csharp +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Entities; +using Microsoft.Extensions.Logging; +using System.Net; + +namespace MyDurableFunctions; + +public class Functions +{ + private readonly ILogger _logger; + + public Functions(ILogger logger) + { + _logger = logger; + } + + // HTTP Starter + [Function("HttpStart")] + public async Task HttpStart( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "orchestrators/{functionName}")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + string functionName, + FunctionContext executionContext) + { + string? requestBody = await new StreamReader(req.Body).ReadToEndAsync(); + + var options = new StartOrchestrationOptions + { + InstanceId = req.Headers.TryGetValues("X-Instance-Id", out var values) + ? values.First() + : null + }; + + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( + functionName, requestBody, options); + + _logger.LogInformation("Started orchestration {FunctionName} with ID = {InstanceId}", + functionName, instanceId); + + return await client.CreateCheckStatusResponseAsync(req, instanceId); + } + + // Get Status + [Function("GetStatus")] + public async Task GetStatus( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "orchestrators/{instanceId}/status")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + string instanceId) + { + var instance = await client.GetInstanceAsync(instanceId, getInputsAndOutputs: true); + + if (instance == null) + { + return req.CreateResponse(HttpStatusCode.NotFound); + } + + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(new + { + instanceId = instance.InstanceId, + name = instance.Name, + runtimeStatus = instance.RuntimeStatus.ToString(), + createdTime = instance.CreatedAt, + lastUpdatedTime = instance.LastUpdatedAt, + input = instance.SerializedInput, + output = instance.SerializedOutput, + customStatus = instance.SerializedCustomStatus + }); + return response; + } + + // Raise Event + [Function("RaiseEvent")] + public async Task RaiseEvent( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "orchestrators/{instanceId}/events/{eventName}")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + string instanceId, + string eventName) + { + string? eventData = await new StreamReader(req.Body).ReadToEndAsync(); + + await client.RaiseEventAsync(instanceId, eventName, eventData); + + return req.CreateResponse(HttpStatusCode.Accepted); + } + + // Terminate + [Function("Terminate")] + public async Task Terminate( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "orchestrators/{instanceId}/terminate")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + string instanceId) + { + string? reason = await new StreamReader(req.Body).ReadToEndAsync(); + + await client.TerminateInstanceAsync(instanceId, reason); + + return req.CreateResponse(HttpStatusCode.Accepted); + } + + // Sample Orchestration + [Function(nameof(SampleOrchestration))] + public static async Task SampleOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) + { + ILogger logger = context.CreateReplaySafeLogger(nameof(SampleOrchestration)); + string input = context.GetInput() ?? "World"; + + logger.LogInformation("Starting orchestration with input: {Input}", input); + + var result1 = await context.CallActivityAsync(nameof(SayHello), "Tokyo"); + var result2 = await context.CallActivityAsync(nameof(SayHello), "Seattle"); + var result3 = await context.CallActivityAsync(nameof(SayHello), input); + + return $"{result1}, {result2}, {result3}"; + } + + // Sample Activity + [Function(nameof(SayHello))] + public string SayHello([ActivityTrigger] string name) + { + _logger.LogInformation("Saying hello to {Name}", name); + return $"Hello {name}!"; + } +} +``` + +## Azure Provisioning + +### Azure CLI + +```bash +# Variables +RESOURCE_GROUP="my-durable-functions-rg" +LOCATION="eastus" +STORAGE_ACCOUNT="mydurablefuncssa" +FUNCTION_APP="my-durable-functions" +DTS_NAMESPACE="my-dts-namespace" +DTS_SCHEDULER="my-scheduler" +TASKHUB_NAME="default" + +# Create resource group +az group create --name $RESOURCE_GROUP --location $LOCATION + +# Create storage account (required for Azure Functions) +az storage account create \ + --name $STORAGE_ACCOUNT \ + --location $LOCATION \ + --resource-group $RESOURCE_GROUP \ + --sku Standard_LRS + +# Create Durable Task Scheduler namespace +az durabletask namespace create \ + --name $DTS_NAMESPACE \ + --resource-group $RESOURCE_GROUP \ + --location $LOCATION \ + --sku "Basic" + +# Create scheduler within namespace +az durabletask scheduler create \ + --name $DTS_SCHEDULER \ + --namespace-name $DTS_NAMESPACE \ + --resource-group $RESOURCE_GROUP \ + --ip-allow-list "[{\"name\": \"AllowAll\", \"startIPAddress\": \"0.0.0.0\", \"endIPAddress\": \"255.255.255.255\"}]" + +# Create task hub +az durabletask taskhub create \ + --name $TASKHUB_NAME \ + --namespace-name $DTS_NAMESPACE \ + --resource-group $RESOURCE_GROUP + +# Get scheduler endpoint +DTS_ENDPOINT=$(az durabletask scheduler show \ + --name $DTS_SCHEDULER \ + --namespace-name $DTS_NAMESPACE \ + --resource-group $RESOURCE_GROUP \ + --query "endpoint" -o tsv) + +# Create Function App (Consumption plan) +az functionapp create \ + --name $FUNCTION_APP \ + --storage-account $STORAGE_ACCOUNT \ + --resource-group $RESOURCE_GROUP \ + --consumption-plan-location $LOCATION \ + --runtime dotnet-isolated \ + --runtime-version 8 \ + --functions-version 4 \ + --assign-identity "[system]" + +# Get Function App identity +FUNCTION_APP_IDENTITY=$(az functionapp identity show \ + --name $FUNCTION_APP \ + --resource-group $RESOURCE_GROUP \ + --query "principalId" -o tsv) + +# Assign Durable Task Contributor role to Function App +DTS_NAMESPACE_ID=$(az durabletask namespace show \ + --name $DTS_NAMESPACE \ + --resource-group $RESOURCE_GROUP \ + --query "id" -o tsv) + +az role assignment create \ + --assignee $FUNCTION_APP_IDENTITY \ + --role "Durable Task Data Contributor" \ + --scope $DTS_NAMESPACE_ID + +# Configure app settings +az functionapp config appsettings set \ + --name $FUNCTION_APP \ + --resource-group $RESOURCE_GROUP \ + --settings \ + "DTS_CONNECTION_STRING=Endpoint=${DTS_ENDPOINT};Authentication=ManagedIdentity" \ + "TASKHUB_NAME=$TASKHUB_NAME" +``` + +### Bicep Template + +```bicep +// main.bicep +@description('The location for all resources') +param location string = resourceGroup().location + +@description('Base name for all resources') +param baseName string = 'mydurablefunc' + +// Storage Account +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: '${baseName}sa' + location: location + kind: 'StorageV2' + sku: { + name: 'Standard_LRS' + } +} + +// Durable Task Scheduler Namespace +resource dtsNamespace 'Microsoft.DurableTask/namespaces@2024-10-01-preview' = { + name: '${baseName}-dts' + location: location + sku: { + name: 'Basic' + capacity: 1 + } + properties: {} +} + +// Scheduler +resource scheduler 'Microsoft.DurableTask/namespaces/schedulers@2024-10-01-preview' = { + parent: dtsNamespace + name: 'scheduler' + location: location + properties: { + ipAllowlist: [ + { + name: 'AllowAll' + startIPAddress: '0.0.0.0' + endIPAddress: '255.255.255.255' + } + ] + } +} + +// Task Hub +resource taskHub 'Microsoft.DurableTask/namespaces/taskHubs@2024-10-01-preview' = { + parent: dtsNamespace + name: 'default' + properties: {} +} + +// App Service Plan +resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = { + name: '${baseName}-plan' + location: location + sku: { + name: 'Y1' + tier: 'Dynamic' + } + properties: {} +} + +// Function App +resource functionApp 'Microsoft.Web/sites@2023-01-01' = { + name: '${baseName}-func' + location: location + kind: 'functionapp' + identity: { + type: 'SystemAssigned' + } + properties: { + serverFarmId: appServicePlan.id + siteConfig: { + netFrameworkVersion: 'v8.0' + appSettings: [ + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'dotnet-isolated' + } + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' + } + { + name: 'AzureWebJobsStorage' + value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' + } + { + name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' + value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' + } + { + name: 'DTS_CONNECTION_STRING' + value: 'Endpoint=${scheduler.properties.endpoint};Authentication=ManagedIdentity' + } + { + name: 'TASKHUB_NAME' + value: 'default' + } + ] + } + } +} + +// Role Assignment - Durable Task Data Contributor +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(dtsNamespace.id, functionApp.id, 'DurableTaskDataContributor') + scope: dtsNamespace + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '9d3a82f5-2d5a-4c3a-8e7f-6c2f8f8f8f8f') // Durable Task Data Contributor + principalId: functionApp.identity.principalId + principalType: 'ServicePrincipal' + } +} + +output functionAppName string = functionApp.name +output functionAppUrl string = 'https://${functionApp.properties.defaultHostName}' +output dtsEndpoint string = scheduler.properties.endpoint +``` + +```bash +# Deploy +az deployment group create \ + --resource-group $RESOURCE_GROUP \ + --template-file main.bicep \ + --parameters baseName=mydurablefunc +``` + +## Deployment + +### Deploy Function App + +```bash +# Build and publish +dotnet publish -c Release -o ./publish + +# Create zip +cd publish +zip -r ../deploy.zip . +cd .. + +# Deploy to Azure +az functionapp deployment source config-zip \ + --resource-group $RESOURCE_GROUP \ + --name $FUNCTION_APP \ + --src deploy.zip +``` + +### GitHub Actions Deployment + +```yaml +# .github/workflows/deploy.yml +name: Deploy Azure Functions + +on: + push: + branches: [main] + +env: + AZURE_FUNCTIONAPP_NAME: 'my-durable-functions' + AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' + DOTNET_VERSION: '8.0.x' + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Build + run: dotnet build --configuration Release + + - name: Publish + run: dotnet publish -c Release -o ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/output + + - name: Deploy to Azure Functions + uses: Azure/functions-action@v1 + with: + app-name: ${{ env.AZURE_FUNCTIONAPP_NAME }} + package: '${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/output' + publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }} +``` + +## Deployment Options + +### Azure Container Apps + +```bash +# Create environment +az containerapp env create \ + --name $ENVIRONMENT_NAME \ + --resource-group $RESOURCE_GROUP \ + --location $LOCATION + +# Build and push image +az acr build \ + --registry $ACR_NAME \ + --image $IMAGE_NAME:$TAG . + +# Deploy +az containerapp create \ + --name $FUNCTION_APP \ + --resource-group $RESOURCE_GROUP \ + --environment $ENVIRONMENT_NAME \ + --image $ACR_NAME.azurecr.io/$IMAGE_NAME:$TAG \ + --target-port 80 \ + --ingress 'external' \ + --min-replicas 1 \ + --max-replicas 10 \ + --env-vars \ + "FUNCTIONS_WORKER_RUNTIME=dotnet-isolated" \ + "AzureWebJobsStorage=$STORAGE_CONNECTION_STRING" \ + "DTS_CONNECTION_STRING=Endpoint=${DTS_ENDPOINT};Authentication=ManagedIdentity" \ + "TASKHUB_NAME=$TASKHUB_NAME" \ + --user-assigned $MANAGED_IDENTITY_ID +``` + +### Dockerfile + +```dockerfile +FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0 AS base +WORKDIR /home/site/wwwroot +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ["MyDurableFunctions.csproj", "."] +RUN dotnet restore +COPY . . +RUN dotnet build -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish -c Release -o /app/publish + +FROM base AS final +WORKDIR /home/site/wwwroot +COPY --from=publish /app/publish . +ENV AzureWebJobsScriptRoot=/home/site/wwwroot +ENV AzureFunctionsJobHost__Logging__Console__IsEnabled=true +``` + +## Testing + +### Unit Testing with Moq + +```csharp +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Moq; +using Xunit; + +public class OrchestratorTests +{ + [Fact] + public async Task SampleOrchestration_ReturnsExpectedResult() + { + // Arrange + var contextMock = new Mock(); + + contextMock.Setup(x => x.GetInput()).Returns("Test"); + + contextMock + .Setup(x => x.CallActivityAsync(nameof(Functions.SayHello), "Tokyo", It.IsAny())) + .ReturnsAsync("Hello Tokyo!"); + + contextMock + .Setup(x => x.CallActivityAsync(nameof(Functions.SayHello), "Seattle", It.IsAny())) + .ReturnsAsync("Hello Seattle!"); + + contextMock + .Setup(x => x.CallActivityAsync(nameof(Functions.SayHello), "Test", It.IsAny())) + .ReturnsAsync("Hello Test!"); + + contextMock + .Setup(x => x.CreateReplaySafeLogger(It.IsAny())) + .Returns(Mock.Of()); + + // Act + var result = await Functions.SampleOrchestration(contextMock.Object); + + // Assert + Assert.Equal("Hello Tokyo!, Hello Seattle!, Hello Test!", result); + } +} +``` + +### Integration Testing + +```csharp +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask.Client; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +public class IntegrationTests : IAsyncLifetime +{ + private IHost _host = null!; + private DurableTaskClient _client = null!; + + public async Task InitializeAsync() + { + _host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .Build(); + + await _host.StartAsync(); + + _client = _host.Services.GetRequiredService(); + } + + public async Task DisposeAsync() + { + await _host.StopAsync(); + _host.Dispose(); + } + + [Fact] + public async Task Orchestration_Completes_Successfully() + { + // Schedule orchestration + string instanceId = await _client.ScheduleNewOrchestrationInstanceAsync( + nameof(Functions.SampleOrchestration), "IntegrationTest"); + + // Wait for completion + var result = await _client.WaitForInstanceCompletionAsync( + instanceId, + getInputsAndOutputs: true, + cancellationToken: new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token); + + Assert.Equal(OrchestrationRuntimeStatus.Completed, result!.RuntimeStatus); + } +} +``` + +## Monitoring and Logging + +### Application Insights + +```csharp +// Program.cs +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureServices(services => + { + services.AddApplicationInsightsTelemetryWorkerService(); + services.ConfigureFunctionsApplicationInsights(); + + // Custom telemetry processor + services.AddSingleton(); + }) + .Build(); + +await host.RunAsync(); + +public class CustomTelemetryInitializer : ITelemetryInitializer +{ + public void Initialize(ITelemetry telemetry) + { + telemetry.Context.Cloud.RoleName = "MyDurableFunctions"; + } +} +``` + +### Custom Status and Metrics + +```csharp +[Function(nameof(TrackedOrchestration))] +public static async Task TrackedOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + ILogger logger = context.CreateReplaySafeLogger(nameof(TrackedOrchestration)); + + // Set progress status + context.SetCustomStatus(new { Stage = "Starting", Progress = 0 }); + + await context.CallActivityAsync(nameof(Step1), null); + context.SetCustomStatus(new { Stage = "Step1Complete", Progress = 33 }); + + await context.CallActivityAsync(nameof(Step2), null); + context.SetCustomStatus(new { Stage = "Step2Complete", Progress = 66 }); + + await context.CallActivityAsync(nameof(Step3), null); + context.SetCustomStatus(new { Stage = "Completed", Progress = 100 }); + + return "Done"; +} +``` + +### KQL Queries for Monitoring + +```kql +// Orchestration completion times +traces +| where message contains "orchestration" +| summarize avg(duration) by bin(timestamp, 1h) + +// Failed orchestrations +traces +| where severityLevel >= 3 +| where message contains "orchestration" or message contains "activity" +| project timestamp, message, severityLevel + +// Activity execution times +customMetrics +| where name == "ActivityDuration" +| summarize avg(value), percentile(value, 95) by bin(timestamp, 5m) +``` diff --git a/.claude/skills/durable-task-dotnet/SKILL.md b/.claude/skills/durable-task-dotnet/SKILL.md new file mode 100644 index 0000000..de1598d --- /dev/null +++ b/.claude/skills/durable-task-dotnet/SKILL.md @@ -0,0 +1,215 @@ +--- +name: durable-task-dotnet +description: Build durable, fault-tolerant workflows in .NET using the Durable Task SDK with Azure Durable Task Scheduler. Use when creating orchestrations, activities, entities, or implementing patterns like function chaining, fan-out/fan-in, human interaction, or stateful agents. Applies to any .NET application requiring durable execution, state persistence, or distributed transactions without Azure Functions dependency. +--- + +# Durable Task .NET SDK with Durable Task Scheduler + +Build fault-tolerant, stateful workflows in .NET applications using the Durable Task SDK connected to Azure Durable Task Scheduler. + +## Quick Start + +### Required NuGet Packages + +```xml + + + + + + +``` + +### Minimal Worker Setup + +```csharp +using Microsoft.DurableTask; +using Microsoft.DurableTask.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = Host.CreateApplicationBuilder(args); + +// Connection string format: "Endpoint={url};TaskHub={name};Authentication={type}" +var connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") + ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; + +builder.Services.AddDurableTaskWorker() + .AddTasks(registry => + { + registry.AddAllGeneratedTasks(); // Registers all [DurableTask] decorated classes + }) + .UseDurableTaskScheduler(connectionString); + +var host = builder.Build(); +await host.RunAsync(); +``` + +### Minimal Client Setup + +```csharp +using Microsoft.DurableTask.Client; + +var connectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING") + ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; + +var client = DurableTaskClientBuilder.UseDurableTaskScheduler(connectionString).Build(); + +// Schedule an orchestration +string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("MyOrchestration", input); + +// Wait for completion +var result = await client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true); +``` + +## Pattern Selection Guide + +| Pattern | Use When | +|---------|----------| +| **Function Chaining** | Sequential steps where each depends on the previous | +| **Fan-Out/Fan-In** | Parallel processing with aggregated results | +| **Human Interaction** | Workflow pauses for external input/approval | +| **Durable Entities** | Stateful objects with operations (counters, accounts) | +| **Sub-Orchestrations** | Reusable workflow components or version isolation | + +See [references/patterns.md](references/patterns.md) for detailed implementations. + +## Orchestration Structure + +### Basic Orchestration + +```csharp +[DurableTask(nameof(MyOrchestration))] +public class MyOrchestration : TaskOrchestrator +{ + public override async Task RunAsync(TaskOrchestrationContext context, string input) + { + // Call activities + var result1 = await context.CallActivityAsync(nameof(Step1Activity), input); + var result2 = await context.CallActivityAsync(nameof(Step2Activity), result1); + return result2; + } +} +``` + +### Basic Activity + +```csharp +[DurableTask(nameof(MyActivity))] +public class MyActivity : TaskActivity +{ + private readonly ILogger _logger; + + public MyActivity(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public override Task RunAsync(TaskActivityContext context, string input) + { + _logger.LogInformation("Processing: {Input}", input); + return Task.FromResult($"Processed: {input}"); + } +} +``` + +## Critical Rules + +### Orchestration Determinism +Orchestrations replay from history - all code MUST be deterministic: + +**NEVER do inside orchestrations:** +- `DateTime.Now`, `DateTime.UtcNow` → Use `context.CurrentUtcDateTime` +- `Guid.NewGuid()` → Use `context.NewGuid()` +- `Random` → Pass random values from activities +- Direct I/O, HTTP calls, database access → Move to activities +- `Task.Delay()` → Use `context.CreateTimer()` +- Non-deterministic LINQ (parallel, unordered) + +**ALWAYS safe:** +- `context.CallActivityAsync()` +- `context.CallSubOrchestrationAsync()` +- `context.CreateTimer()` +- `context.WaitForExternalEvent()` +- `context.CurrentUtcDateTime` +- `context.NewGuid()` +- `context.SetCustomStatus()` + +### Error Handling + +```csharp +public override async Task RunAsync(TaskOrchestrationContext context, string input) +{ + try + { + return await context.CallActivityAsync(nameof(RiskyActivity), input); + } + catch (TaskFailedException ex) + { + // Activity failed - implement compensation or retry + context.SetCustomStatus(new { Error = ex.Message }); + return await context.CallActivityAsync(nameof(CompensationActivity), input); + } +} +``` + +### Retry Policies + +```csharp +var options = new TaskOptions +{ + Retry = new RetryPolicy( + maxNumberOfAttempts: 3, + firstRetryInterval: TimeSpan.FromSeconds(5), + backoffCoefficient: 2.0, + maxRetryInterval: TimeSpan.FromMinutes(1)) +}; + +await context.CallActivityAsync(nameof(UnreliableActivity), input, options); +``` + +## Connection & Authentication + +### Connection String Formats + +```csharp +// Local emulator (no auth) +"Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" + +// Azure with DefaultAzureCredential +"Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=DefaultAzure" + +// Azure with Managed Identity +"Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=ManagedIdentity" + +// Azure with specific credential +"Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=AzureCLI" +``` + +### Authentication Helper + +```csharp +static string GetConnectionString() +{ + var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:8080"; + var taskHub = Environment.GetEnvironmentVariable("TASKHUB") ?? "default"; + + var authType = endpoint.StartsWith("http://localhost") ? "None" : "DefaultAzure"; + return $"Endpoint={endpoint};TaskHub={taskHub};Authentication={authType}"; +} +``` + +## Local Development with Emulator + +```bash +# Pull and run the emulator +docker pull mcr.microsoft.com/dts/dts-emulator:latest +docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest + +# Dashboard available at http://localhost:8082 +``` + +## References + +- **[patterns.md](references/patterns.md)** - Detailed pattern implementations (Fan-Out/Fan-In, Human Interaction, Entities, Sub-Orchestrations) +- **[setup.md](references/setup.md)** - Azure Durable Task Scheduler provisioning and deployment diff --git a/.claude/skills/durable-task-dotnet/references/patterns.md b/.claude/skills/durable-task-dotnet/references/patterns.md new file mode 100644 index 0000000..8e557e0 --- /dev/null +++ b/.claude/skills/durable-task-dotnet/references/patterns.md @@ -0,0 +1,575 @@ +# Durable Task Workflow Patterns + +## Table of Contents +- [Function Chaining](#function-chaining) +- [Fan-Out/Fan-In](#fan-outfan-in) +- [Human Interaction](#human-interaction) +- [Durable Entities](#durable-entities) +- [Sub-Orchestrations](#sub-orchestrations) +- [Scheduling/Timers](#schedulingtimers) + +--- + +## Function Chaining + +Sequential execution where output of one activity feeds into the next. + +### Implementation + +```csharp +[DurableTask(nameof(OrderProcessingOrchestration))] +public class OrderProcessingOrchestration : TaskOrchestrator +{ + public override async Task RunAsync(TaskOrchestrationContext context, OrderRequest order) + { + // Step 1: Validate order + var validationResult = await context.CallActivityAsync( + nameof(ValidateOrderActivity), order); + + if (!validationResult.IsValid) + return new OrderResult { Status = "ValidationFailed", Message = validationResult.Error }; + + // Step 2: Process payment + var paymentResult = await context.CallActivityAsync( + nameof(ProcessPaymentActivity), order); + + // Step 3: Reserve inventory + var inventoryResult = await context.CallActivityAsync( + nameof(ReserveInventoryActivity), new ReservationRequest + { + OrderId = order.Id, + PaymentId = paymentResult.TransactionId + }); + + // Step 4: Ship order + var shippingResult = await context.CallActivityAsync( + nameof(ShipOrderActivity), new ShipRequest + { + OrderId = order.Id, + InventoryReservationId = inventoryResult.ReservationId + }); + + return new OrderResult + { + Status = "Completed", + TrackingNumber = shippingResult.TrackingNumber + }; + } +} + +[DurableTask(nameof(ValidateOrderActivity))] +public class ValidateOrderActivity : TaskActivity +{ + public override Task RunAsync(TaskActivityContext context, OrderRequest order) + { + // Validation logic + if (order.Items == null || !order.Items.Any()) + return Task.FromResult(new ValidationResult { IsValid = false, Error = "No items in order" }); + + return Task.FromResult(new ValidationResult { IsValid = true }); + } +} +``` + +--- + +## Fan-Out/Fan-In + +Execute multiple activities in parallel, then aggregate results. + +### Implementation + +```csharp +[DurableTask(nameof(BatchProcessingOrchestration))] +public class BatchProcessingOrchestration : TaskOrchestrator, ProcessingResults> +{ + public override async Task RunAsync( + TaskOrchestrationContext context, List workItems) + { + // Fan-out: create parallel tasks + var parallelTasks = new List>(); + + foreach (var item in workItems) + { + var task = context.CallActivityAsync( + nameof(ProcessItemActivity), item); + parallelTasks.Add(task); + } + + // Wait for all parallel tasks + var results = await Task.WhenAll(parallelTasks); + + // Fan-in: aggregate results + var aggregatedResult = await context.CallActivityAsync( + nameof(AggregateResultsActivity), results); + + return aggregatedResult; + } +} + +[DurableTask(nameof(ProcessItemActivity))] +public class ProcessItemActivity : TaskActivity +{ + private readonly ILogger _logger; + + public ProcessItemActivity(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public override async Task RunAsync(TaskActivityContext context, string item) + { + _logger.LogInformation("Processing item: {Item}", item); + + // Simulate processing + await Task.Delay(TimeSpan.FromMilliseconds(100)); + + return new ItemResult + { + ItemId = item, + ProcessedValue = item.Length, + ProcessedAt = DateTime.UtcNow + }; + } +} + +[DurableTask(nameof(AggregateResultsActivity))] +public class AggregateResultsActivity : TaskActivity +{ + public override Task RunAsync( + TaskActivityContext context, ItemResult[] results) + { + return Task.FromResult(new ProcessingResults + { + TotalItems = results.Length, + TotalValue = results.Sum(r => r.ProcessedValue), + ItemResults = results.ToList() + }); + } +} +``` + +### With Batching for Large Workloads + +```csharp +public override async Task RunAsync( + TaskOrchestrationContext context, List workItems) +{ + const int batchSize = 10; + var allResults = new List(); + + // Process in batches to avoid overwhelming resources + for (int i = 0; i < workItems.Count; i += batchSize) + { + var batch = workItems.Skip(i).Take(batchSize).ToList(); + + var batchTasks = batch.Select(item => + context.CallActivityAsync(nameof(ProcessItemActivity), item)); + + var batchResults = await Task.WhenAll(batchTasks); + allResults.AddRange(batchResults); + + // Update progress + context.SetCustomStatus(new { + Processed = Math.Min(i + batchSize, workItems.Count), + Total = workItems.Count + }); + } + + return await context.CallActivityAsync( + nameof(AggregateResultsActivity), allResults.ToArray()); +} +``` + +--- + +## Human Interaction + +Pause workflow for external approval or input. + +### Implementation + +```csharp +[DurableTask(nameof(ApprovalOrchestration))] +public class ApprovalOrchestration : TaskOrchestrator +{ + private const string ApprovalEventName = "approval_response"; + + public override async Task RunAsync( + TaskOrchestrationContext context, ApprovalRequest request) + { + // Step 1: Submit approval request + var submission = await context.CallActivityAsync( + nameof(SubmitApprovalRequestActivity), request); + + // Make request details available via custom status + context.SetCustomStatus(new { + RequestId = request.Id, + Status = "PendingApproval", + SubmittedAt = submission.SubmittedAt, + ApprovalUrl = submission.ApprovalUrl + }); + + // Step 2: Wait for approval or timeout + var timeout = context.CurrentUtcDateTime.Add(request.TimeoutDuration); + + using var timeoutCts = new CancellationTokenSource(); + var timeoutTask = context.CreateTimer(timeout, timeoutCts.Token); + var approvalTask = context.WaitForExternalEvent(ApprovalEventName); + + var completedTask = await Task.WhenAny(approvalTask, timeoutTask); + + ApprovalResult result; + + if (completedTask == approvalTask) + { + // Approval received before timeout + timeoutCts.Cancel(); + var response = approvalTask.Result; + + result = await context.CallActivityAsync( + nameof(ProcessApprovalActivity), new ProcessApprovalInput + { + Request = request, + Response = response + }); + } + else + { + // Timeout occurred + result = new ApprovalResult + { + RequestId = request.Id, + Status = ApprovalStatus.TimedOut, + ProcessedAt = context.CurrentUtcDateTime + }; + } + + // Notify requester of outcome + await context.CallActivityAsync(nameof(SendNotificationActivity), new NotificationInput + { + RequestId = request.Id, + Result = result + }); + + return result; + } +} + +// Client code to send approval +public async Task ApproveRequest(DurableTaskClient client, string instanceId, bool approved) +{ + var response = new ApprovalResponse + { + IsApproved = approved, + ApprovedBy = "user@example.com", + Comments = approved ? "Looks good!" : "Rejected due to policy violation" + }; + + await client.RaiseEventAsync(instanceId, "approval_response", response); +} +``` + +### Multi-Step Approval + +```csharp +public override async Task RunAsync( + TaskOrchestrationContext context, MultiLevelApprovalRequest request) +{ + var approvers = new[] { "manager", "director", "vp" }; + + foreach (var level in approvers) + { + if (request.Amount < GetThresholdForLevel(level)) + continue; + + context.SetCustomStatus(new { + CurrentLevel = level, + PendingApproval = true + }); + + // Wait for approval at this level + var response = await context.WaitForExternalEvent($"approval_{level}"); + + if (!response.IsApproved) + { + return new ApprovalResult { Status = ApprovalStatus.Rejected, RejectedBy = level }; + } + } + + return new ApprovalResult { Status = ApprovalStatus.Approved }; +} +``` + +--- + +## Durable Entities + +Stateful objects for managing state with atomic operations. + +### Entity Definition + +```csharp +public interface IAccountEntity +{ + void Deposit(decimal amount); + void Withdraw(decimal amount); + decimal GetBalance(); + void Reset(); +} + +[DurableTask(nameof(AccountEntity))] +public class AccountEntity : TaskEntity, IAccountEntity +{ + public void Deposit(decimal amount) + { + if (amount <= 0) + throw new ArgumentException("Amount must be positive"); + + State.Balance += amount; + State.LastModified = DateTime.UtcNow; + State.TransactionHistory.Add(new Transaction + { + Type = "Deposit", + Amount = amount, + Timestamp = State.LastModified + }); + } + + public void Withdraw(decimal amount) + { + if (amount <= 0) + throw new ArgumentException("Amount must be positive"); + + if (State.Balance < amount) + throw new InvalidOperationException("Insufficient funds"); + + State.Balance -= amount; + State.LastModified = DateTime.UtcNow; + State.TransactionHistory.Add(new Transaction + { + Type = "Withdrawal", + Amount = amount, + Timestamp = State.LastModified + }); + } + + public decimal GetBalance() => State.Balance; + + public void Reset() + { + State = new AccountState(); + } + + protected override AccountState InitializeState() => new AccountState(); +} + +public class AccountState +{ + public decimal Balance { get; set; } + public DateTime LastModified { get; set; } + public List TransactionHistory { get; set; } = new(); +} +``` + +### Using Entities from Orchestrations + +```csharp +[DurableTask(nameof(TransferFundsOrchestration))] +public class TransferFundsOrchestration : TaskOrchestrator +{ + public override async Task RunAsync( + TaskOrchestrationContext context, TransferRequest request) + { + var sourceEntity = new EntityInstanceId(nameof(AccountEntity), request.SourceAccountId); + var destEntity = new EntityInstanceId(nameof(AccountEntity), request.DestinationAccountId); + + // Check source balance + var sourceBalance = await context.Entities.CallEntityAsync( + sourceEntity, nameof(IAccountEntity.GetBalance)); + + if (sourceBalance < request.Amount) + { + return new TransferResult + { + Success = false, + Error = "Insufficient funds" + }; + } + + // Perform transfer atomically using critical section + using (await context.Entities.LockEntitiesAsync(sourceEntity, destEntity)) + { + try + { + // Withdraw from source + await context.Entities.CallEntityAsync( + sourceEntity, nameof(IAccountEntity.Withdraw), request.Amount); + + // Deposit to destination + await context.Entities.CallEntityAsync( + destEntity, nameof(IAccountEntity.Deposit), request.Amount); + + return new TransferResult + { + Success = true, + TransactionId = context.NewGuid().ToString() + }; + } + catch (Exception ex) + { + // Compensation logic if needed + return new TransferResult { Success = false, Error = ex.Message }; + } + } + } +} +``` + +### Signaling Entities from Client + +```csharp +// Fire-and-forget signal (one-way) +await client.Entities.SignalEntityAsync( + new EntityInstanceId(nameof(AccountEntity), "account-123"), + nameof(IAccountEntity.Deposit), + 100.00m); + +// Query entity state +var balance = await client.Entities.GetEntityAsync( + new EntityInstanceId(nameof(AccountEntity), "account-123")); +``` + +--- + +## Sub-Orchestrations + +Compose orchestrations for modularity and version management. + +### Implementation + +```csharp +[DurableTask(nameof(MainOrchestration))] +public class MainOrchestration : TaskOrchestrator +{ + public override async Task RunAsync( + TaskOrchestrationContext context, MainRequest request) + { + // Call sub-orchestration + var orderResult = await context.CallSubOrchestrationAsync( + nameof(OrderProcessingOrchestration), + new OrderRequest { CustomerId = request.CustomerId, Items = request.Items }); + + // Call another sub-orchestration with custom instance ID + var notificationResult = await context.CallSubOrchestrationAsync( + nameof(NotificationOrchestration), + new NotificationRequest { OrderId = orderResult.OrderId }, + new SubOrchestrationOptions + { + InstanceId = $"notification-{orderResult.OrderId}" + }); + + return new MainResult + { + OrderResult = orderResult, + NotificationResult = notificationResult + }; + } +} +``` + +### Parallel Sub-Orchestrations + +```csharp +public override async Task RunAsync( + TaskOrchestrationContext context, List customerIds) +{ + var tasks = customerIds.Select(customerId => + context.CallSubOrchestrationAsync( + nameof(ProcessCustomerOrchestration), + new CustomerRequest { CustomerId = customerId }, + new SubOrchestrationOptions { InstanceId = $"customer-{customerId}" })); + + var results = await Task.WhenAll(tasks); + + return new BatchResult { CustomerResults = results.ToList() }; +} +``` + +--- + +## Scheduling/Timers + +Implement delays, schedules, and recurring workflows. + +### Delayed Execution + +```csharp +public override async Task RunAsync( + TaskOrchestrationContext context, ReminderRequest request) +{ + // Wait until specified time + var reminderTime = request.ReminderDateTime; + await context.CreateTimer(reminderTime, CancellationToken.None); + + // Send reminder + return await context.CallActivityAsync( + nameof(SendReminderActivity), request); +} +``` + +### Recurring Schedule (Eternal Orchestration) + +```csharp +[DurableTask(nameof(RecurringJobOrchestration))] +public class RecurringJobOrchestration : TaskOrchestrator +{ + public override async Task RunAsync( + TaskOrchestrationContext context, RecurringJobConfig config) + { + // Execute the job + await context.CallActivityAsync(nameof(ExecuteJobActivity), config.JobData); + + // Calculate next run time + var nextRun = context.CurrentUtcDateTime.Add(config.Interval); + + // Wait for next scheduled time + await context.CreateTimer(nextRun, CancellationToken.None); + + // Continue as new to avoid history buildup + context.ContinueAsNew(config); + + return null; + } +} +``` + +### Scheduled with Early Termination + +```csharp +public override async Task RunAsync( + TaskOrchestrationContext context, ScheduledJobConfig config) +{ + while (context.CurrentUtcDateTime < config.EndDate) + { + // Check for cancellation signal + var cancelTask = context.WaitForExternalEvent("cancel"); + var timerTask = context.CreateTimer( + context.CurrentUtcDateTime.Add(config.Interval), + CancellationToken.None); + + var completedTask = await Task.WhenAny(cancelTask, timerTask); + + if (completedTask == cancelTask && cancelTask.Result) + { + // Cancelled + return new { Status = "Cancelled" }; + } + + // Execute job + await context.CallActivityAsync(nameof(ExecuteJobActivity), config.JobData); + } + + return new { Status = "Completed", EndDate = config.EndDate }; +} +``` diff --git a/.claude/skills/durable-task-dotnet/references/setup.md b/.claude/skills/durable-task-dotnet/references/setup.md new file mode 100644 index 0000000..71975ed --- /dev/null +++ b/.claude/skills/durable-task-dotnet/references/setup.md @@ -0,0 +1,573 @@ +# Azure Durable Task Scheduler Setup + +## Table of Contents +- [Local Development with Emulator](#local-development-with-emulator) +- [Azure Deployment](#azure-deployment) +- [Authentication Configuration](#authentication-configuration) +- [ASP.NET Integration](#aspnet-integration) +- [Worker Console App](#worker-console-app) +- [Deployment Options](#deployment-options) + +--- + +## Local Development with Emulator + +### Docker Setup + +```bash +# Pull the emulator image +docker pull mcr.microsoft.com/dts/dts-emulator:latest + +# Run the emulator +docker run -d \ + --name dts-emulator \ + -p 8080:8080 \ + -p 8082:8082 \ + mcr.microsoft.com/dts/dts-emulator:latest + +# Ports: +# - 8080: gRPC endpoint for worker/client connections +# - 8082: Dashboard UI + +# View logs +docker logs -f dts-emulator + +# Stop emulator +docker stop dts-emulator + +# Remove container +docker rm dts-emulator +``` + +### Emulator Connection String + +```csharp +var connectionString = "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; +``` + +### Dashboard Access + +Navigate to `http://localhost:8082` to view: +- Orchestration instances +- Execution history +- Entity states +- Activity progress + +--- + +## Azure Deployment + +### Prerequisites + +```bash +# Install/upgrade Azure CLI +az upgrade + +# Install Durable Task CLI extension +az extension add --name durabletask --allow-preview true + +# Login to Azure +az login +``` + +### Create Resources + +```bash +# Set variables +RESOURCE_GROUP="my-dts-rg" +LOCATION="eastus" # Check available regions first +SCHEDULER_NAME="my-dts-scheduler" +TASKHUB_NAME="my-taskhub" + +# Check available regions +az provider show \ + --namespace Microsoft.DurableTask \ + --query "resourceTypes[?resourceType=='schedulers'].locations | [0]" \ + --out table + +# Create resource group +az group create \ + --name $RESOURCE_GROUP \ + --location $LOCATION + +# Create scheduler +az durabletask scheduler create \ + --resource-group $RESOURCE_GROUP \ + --name $SCHEDULER_NAME \ + --ip-allowlist '["0.0.0.0/0"]' \ + --sku-name "Dedicated" \ + --sku-capacity 1 \ + --tags "environment=development" + +# Create task hub +az durabletask taskhub create \ + --resource-group $RESOURCE_GROUP \ + --scheduler-name $SCHEDULER_NAME \ + --name $TASKHUB_NAME +``` + +### Grant Access + +```bash +# Get subscription and user info +SUBSCRIPTION_ID=$(az account show --query "id" -o tsv) +USER_EMAIL=$(az account show --query "user.name" -o tsv) + +# Grant "Durable Task Data Contributor" role +az role assignment create \ + --assignee $USER_EMAIL \ + --role "Durable Task Data Contributor" \ + --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.DurableTask/schedulers/$SCHEDULER_NAME/taskHubs/$TASKHUB_NAME" + +# Note: Role assignment may take 1-2 minutes to propagate +``` + +### Get Connection Details + +```bash +# Get scheduler endpoint +ENDPOINT=$(az durabletask scheduler show \ + --resource-group $RESOURCE_GROUP \ + --name $SCHEDULER_NAME \ + --query "properties.endpoint" \ + --output tsv) + +# Get dashboard URL +DASHBOARD_URL=$(az durabletask taskhub show \ + --resource-group $RESOURCE_GROUP \ + --scheduler-name $SCHEDULER_NAME \ + --name $TASKHUB_NAME \ + --query "properties.dashboardUrl" \ + --output tsv) + +# Set environment variable +export DURABLE_TASK_SCHEDULER_CONNECTION_STRING="Endpoint=$ENDPOINT;TaskHub=$TASKHUB_NAME;Authentication=DefaultAzure" + +echo "Endpoint: $ENDPOINT" +echo "Dashboard: $DASHBOARD_URL" +echo "Connection String: $DURABLE_TASK_SCHEDULER_CONNECTION_STRING" +``` + +--- + +## Authentication Configuration + +### Authentication Types + +| Type | Use Case | Connection String Value | +|------|----------|------------------------| +| None | Local emulator only | `Authentication=None` | +| DefaultAzure | Auto-detect credential (recommended) | `Authentication=DefaultAzure` | +| ManagedIdentity | Azure hosted apps with MI | `Authentication=ManagedIdentity` | +| WorkloadIdentity | Kubernetes with workload identity | `Authentication=WorkloadIdentity` | +| AzureCLI | Local dev with az login | `Authentication=AzureCLI` | +| Environment | Service principal via env vars | `Authentication=Environment` | + +### DefaultAzureCredential Chain + +The `DefaultAzure` authentication tries these methods in order: +1. Environment variables (service principal) +2. Workload Identity +3. Managed Identity +4. Visual Studio credential +5. Azure CLI credential +6. Azure PowerShell credential +7. Interactive browser (development only) + +### Service Principal (Environment Variables) + +```bash +# Required environment variables for Environment auth +export AZURE_CLIENT_ID="" +export AZURE_TENANT_ID="" +export AZURE_CLIENT_SECRET="" +``` + +### Managed Identity Setup + +```bash +# Create user-assigned managed identity +az identity create \ + --name "dts-app-identity" \ + --resource-group $RESOURCE_GROUP + +# Get identity's principal ID +IDENTITY_PRINCIPAL_ID=$(az identity show \ + --name "dts-app-identity" \ + --resource-group $RESOURCE_GROUP \ + --query "principalId" -o tsv) + +# Grant role to managed identity +az role assignment create \ + --assignee $IDENTITY_PRINCIPAL_ID \ + --role "Durable Task Data Contributor" \ + --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.DurableTask/schedulers/$SCHEDULER_NAME/taskHubs/$TASKHUB_NAME" +``` + +--- + +## ASP.NET Integration + +### Project Setup + +```xml + + + net8.0 + enable + enable + + + + + + + + + +``` + +### Program.cs + +```csharp +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Worker; + +var builder = WebApplication.CreateBuilder(args); + +// Get connection string from configuration +var connectionString = builder.Configuration["DurableTaskScheduler:ConnectionString"] + ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; + +// Add Durable Task Worker (background service) +builder.Services.AddDurableTaskWorker() + .AddTasks(registry => + { + registry.AddAllGeneratedTasks(); + }) + .UseDurableTaskScheduler(connectionString); + +// Add Durable Task Client (for scheduling orchestrations) +builder.Services.AddDurableTaskClient() + .UseDurableTaskScheduler(connectionString); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); +``` + +### Controller Example + +```csharp +using Microsoft.AspNetCore.Mvc; +using Microsoft.DurableTask.Client; + +[ApiController] +[Route("api/[controller]")] +public class WorkflowsController : ControllerBase +{ + private readonly DurableTaskClient _client; + + public WorkflowsController(DurableTaskClient client) + { + _client = client; + } + + [HttpPost("start")] + public async Task StartWorkflow([FromBody] WorkflowRequest request) + { + var instanceId = await _client.ScheduleNewOrchestrationInstanceAsync( + "MyOrchestration", + request.Input); + + return Accepted(new { InstanceId = instanceId }); + } + + [HttpGet("{instanceId}/status")] + public async Task GetStatus(string instanceId) + { + var metadata = await _client.GetInstanceAsync(instanceId, getInputsAndOutputs: true); + + if (metadata == null) + return NotFound(); + + return Ok(new + { + InstanceId = metadata.InstanceId, + Status = metadata.RuntimeStatus.ToString(), + CreatedAt = metadata.CreatedAt, + CompletedAt = metadata.LastUpdatedAt, + Output = metadata.SerializedOutput + }); + } + + [HttpPost("{instanceId}/events/{eventName}")] + public async Task RaiseEvent( + string instanceId, + string eventName, + [FromBody] object eventData) + { + await _client.RaiseEventAsync(instanceId, eventName, eventData); + return Accepted(); + } + + [HttpPost("{instanceId}/terminate")] + public async Task Terminate(string instanceId, [FromBody] string reason) + { + await _client.TerminateInstanceAsync(instanceId, reason); + return Accepted(); + } +} +``` + +### appsettings.json + +```json +{ + "DurableTaskScheduler": { + "ConnectionString": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.DurableTask": "Debug" + } + } +} +``` + +### appsettings.Production.json + +```json +{ + "DurableTaskScheduler": { + "ConnectionString": "Endpoint=https://my-scheduler.eastus.durabletask.io;TaskHub=production;Authentication=ManagedIdentity" + } +} +``` + +--- + +## Worker Console App + +### Project Setup + +```xml + + + Exe + net8.0 + enable + enable + + + + + + + + + +``` + +### Program.cs (Worker Only) + +```csharp +using Microsoft.DurableTask.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var builder = Host.CreateApplicationBuilder(args); + +// Configure logging +builder.Logging.AddConsole(); +builder.Logging.SetMinimumLevel(LogLevel.Information); + +var connectionString = GetConnectionString(); + +builder.Services.AddDurableTaskWorker() + .AddTasks(registry => + { + registry.AddAllGeneratedTasks(); + }) + .UseDurableTaskScheduler(connectionString); + +var host = builder.Build(); + +Console.WriteLine("Starting Durable Task Worker..."); +Console.WriteLine($"Connection: {MaskConnectionString(connectionString)}"); + +await host.RunAsync(); + +static string GetConnectionString() +{ + var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:8080"; + var taskHub = Environment.GetEnvironmentVariable("TASKHUB") ?? "default"; + var authType = endpoint.StartsWith("http://localhost") ? "None" : "DefaultAzure"; + + return $"Endpoint={endpoint};TaskHub={taskHub};Authentication={authType}"; +} + +static string MaskConnectionString(string connStr) +{ + // Don't log full connection string in production + if (connStr.Contains("localhost")) + return connStr; + return connStr.Split(';')[0] + ";..."; +} +``` + +--- + +## Deployment Options + +### Azure Container Apps + +```bash +# Create Container Apps environment +az containerapp env create \ + --name dts-env \ + --resource-group $RESOURCE_GROUP \ + --location $LOCATION + +# Deploy worker app +az containerapp create \ + --name dts-worker \ + --resource-group $RESOURCE_GROUP \ + --environment dts-env \ + --image myregistry.azurecr.io/dts-worker:latest \ + --min-replicas 1 \ + --max-replicas 5 \ + --cpu 0.5 \ + --memory 1.0Gi \ + --user-assigned /subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.ManagedIdentity/userAssignedIdentities/dts-app-identity \ + --env-vars \ + ENDPOINT=$ENDPOINT \ + TASKHUB=$TASKHUB_NAME +``` + +### Azure Kubernetes Service (AKS) + +```yaml +# deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dts-worker +spec: + replicas: 2 + selector: + matchLabels: + app: dts-worker + template: + metadata: + labels: + app: dts-worker + azure.workload.identity/use: "true" + spec: + serviceAccountName: dts-service-account + containers: + - name: worker + image: myregistry.azurecr.io/dts-worker:latest + env: + - name: ENDPOINT + valueFrom: + secretKeyRef: + name: dts-config + key: endpoint + - name: TASKHUB + valueFrom: + secretKeyRef: + name: dts-config + key: taskhub + resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "1000m" + memory: "1Gi" +``` + +### Azure App Service + +```bash +# Create App Service Plan +az appservice plan create \ + --name dts-plan \ + --resource-group $RESOURCE_GROUP \ + --sku B1 \ + --is-linux + +# Create Web App +az webapp create \ + --name dts-webapp \ + --resource-group $RESOURCE_GROUP \ + --plan dts-plan \ + --runtime "DOTNET|8.0" + +# Assign managed identity +az webapp identity assign \ + --name dts-webapp \ + --resource-group $RESOURCE_GROUP + +# Configure app settings +az webapp config appsettings set \ + --name dts-webapp \ + --resource-group $RESOURCE_GROUP \ + --settings \ + DurableTaskScheduler__ConnectionString="Endpoint=$ENDPOINT;TaskHub=$TASKHUB_NAME;Authentication=ManagedIdentity" +``` + +### Docker Compose (Development) + +```yaml +# docker-compose.yml +version: '3.8' + +services: + emulator: + image: mcr.microsoft.com/dts/dts-emulator:latest + ports: + - "8080:8080" + - "8082:8082" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 10s + timeout: 5s + retries: 5 + + worker: + build: ./Worker + depends_on: + emulator: + condition: service_healthy + environment: + - ENDPOINT=http://emulator:8080 + - TASKHUB=default + + api: + build: ./Api + ports: + - "5000:8080" + depends_on: + emulator: + condition: service_healthy + environment: + - DurableTaskScheduler__ConnectionString=Endpoint=http://emulator:8080;TaskHub=default;Authentication=None +``` diff --git a/.claude/skills/durable-task-java/SKILL.md b/.claude/skills/durable-task-java/SKILL.md new file mode 100644 index 0000000..0dccf2b --- /dev/null +++ b/.claude/skills/durable-task-java/SKILL.md @@ -0,0 +1,482 @@ +--- +name: durable-task-java +description: Build durable, fault-tolerant workflows in Java using the Durable Task SDK with Azure Durable Task Scheduler. Use when creating orchestrations, activities, or implementing patterns like function chaining, fan-out/fan-in, human interaction, or monitoring. Applies to any Java application requiring durable execution, state persistence, or distributed transactions without Azure Functions dependency. +--- + +# Durable Task Java SDK with Durable Task Scheduler + +Build fault-tolerant, stateful workflows in Java applications using the Durable Task SDK connected to Azure Durable Task Scheduler. + +## Quick Start + +### Maven Dependencies + +```xml + + + com.microsoft.durabletask + durabletask-client + 1.6.2 + + + com.microsoft.durabletask + durabletask-azuremanaged + 1.6.2 + + + com.azure + azure-identity + 1.11.0 + + +``` + +### Gradle Dependencies + +```groovy +dependencies { + implementation 'com.microsoft.durabletask:durabletask-client:1.6.2' + implementation 'com.microsoft.durabletask:durabletask-azuremanaged:1.6.2' + implementation 'com.azure:azure-identity:1.11.0' +} +``` + +### Minimal Worker + Client Setup + +```java +import com.microsoft.durabletask.*; +import com.microsoft.durabletask.azuremanaged.*; +import java.time.Duration; + +public class DurableTaskApp { + public static void main(String[] args) throws Exception { + // Connection string - defaults to local emulator + String connectionString = System.getenv("DURABLE_TASK_CONNECTION_STRING"); + if (connectionString == null) { + connectionString = "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; + } + + // Build and start the worker + DurableTaskGrpcWorker worker = new DurableTaskGrpcWorkerBuilder() + .connectionString(connectionString) + .addOrchestration("MyOrchestration", ctx -> { + String input = ctx.getInput(String.class); + String result = ctx.callActivity("SayHello", input, String.class).await(); + return result; + }) + .addActivity("SayHello", ctx -> { + String name = ctx.getInput(String.class); + return "Hello " + name + "!"; + }) + .build(); + + worker.start(); + + // Build the client + DurableTaskClient client = new DurableTaskGrpcClientBuilder() + .connectionString(connectionString) + .build(); + + // Schedule an orchestration + String instanceId = client.scheduleNewOrchestrationInstance("MyOrchestration", "World"); + System.out.println("Started orchestration: " + instanceId); + + // Wait for completion + OrchestrationMetadata result = client.waitForInstanceCompletion( + instanceId, Duration.ofSeconds(60), true); + + System.out.println("Result: " + result.readOutputAs(String.class)); + + worker.close(); + client.close(); + } +} +``` + +## Pattern Selection Guide + +| Pattern | Use When | +|---------|----------| +| **Function Chaining** | Sequential steps where each depends on the previous | +| **Fan-Out/Fan-In** | Parallel processing with aggregated results | +| **Human Interaction** | Workflow pauses for external input/approval | +| **Sub-Orchestrations** | Reusable workflow components or version isolation | +| **Eternal Orchestrations** | Long-running background processes with `continueAsNew` | +| **Monitoring** | Periodic polling with configurable timeouts | + +See [references/patterns.md](references/patterns.md) for detailed implementations. + +## Orchestration Structure + +### Basic Orchestrator + +```java +// Orchestrator function - MUST be deterministic +.addOrchestration("OrderWorkflow", ctx -> { + OrderInfo order = ctx.getInput(OrderInfo.class); + + // Call activities sequentially + boolean valid = ctx.callActivity("ValidateOrder", order, Boolean.class).await(); + if (!valid) { + return "Order invalid"; + } + + String result = ctx.callActivity("ProcessOrder", order, String.class).await(); + return result; +}) +``` + +### Basic Activity + +```java +// Activity function - can have side effects, I/O, non-determinism +.addActivity("ProcessOrder", ctx -> { + OrderInfo order = ctx.getInput(OrderInfo.class); + + // Perform actual work here - HTTP calls, database, etc. + System.out.println("Processing order: " + order.getOrderId()); + + return "Order " + order.getOrderId() + " processed"; +}) +``` + +## Critical Rules + +### Orchestration Determinism + +Orchestrations replay from history - all code MUST be deterministic. When an orchestration resumes, it replays all previous code to rebuild state. Non-deterministic code produces different results on replay, causing failures. + +**NEVER do inside orchestrations:** +- `Instant.now()`, `LocalDateTime.now()`, `new Date()` → Use `ctx.getCurrentInstant()` +- `UUID.randomUUID()` → Use `ctx.newUUID()` +- `new Random()` → Pass random values from activities +- Direct I/O, HTTP calls, database access → Move to activities +- `Thread.sleep()` → Use `ctx.createTimer()` +- `System.getenv()` that may change → Pass as input or use activities +- HashMap/HashSet iteration (non-deterministic order) → Use TreeMap/TreeSet + +**ALWAYS use:** +- `ctx.callActivity("Name", input, Type.class).await()` - Call activities +- `ctx.callSubOrchestrator("Name", input, Type.class).await()` - Sub-orchestrations +- `ctx.createTimer(Duration).await()` - Durable delays +- `ctx.waitForExternalEvent("EventName", timeout, Type.class).await()` - External events +- `ctx.getCurrentInstant()` - Current time (deterministic) +- `ctx.newUUID()` - Generate UUIDs (deterministic) +- `ctx.setCustomStatus(status)` - Set status + +### Non-Determinism Patterns (WRONG vs CORRECT) + +#### Getting Current Time + +```java +// WRONG - Instant.now() returns different value on replay +.addOrchestration("BadOrchestration", ctx -> { + Instant currentTime = Instant.now(); // Non-deterministic! + if (currentTime.isBefore(deadline)) { + ctx.callActivity("ProcessNow", null, Void.class).await(); + } + return null; +}) + +// CORRECT - ctx.getCurrentInstant() replays consistently +.addOrchestration("GoodOrchestration", ctx -> { + Instant currentTime = ctx.getCurrentInstant(); // Deterministic + if (currentTime.isBefore(deadline)) { + ctx.callActivity("ProcessNow", null, Void.class).await(); + } + return null; +}) +``` + +#### Generating UUIDs + +```java +// WRONG - UUID.randomUUID() generates different value on replay +.addOrchestration("BadOrchestration", ctx -> { + String orderId = UUID.randomUUID().toString(); // Non-deterministic! + ctx.callActivity("CreateOrder", orderId, Void.class).await(); + return orderId; +}) + +// CORRECT - ctx.newUUID() replays the same value +.addOrchestration("GoodOrchestration", ctx -> { + String orderId = ctx.newUUID().toString(); // Deterministic + ctx.callActivity("CreateOrder", orderId, Void.class).await(); + return orderId; +}) +``` + +#### Random Numbers + +```java +// WRONG - Random produces different values on replay +.addOrchestration("BadOrchestration", ctx -> { + int delay = new Random().nextInt(10); // Non-deterministic! + ctx.createTimer(Duration.ofSeconds(delay)).await(); + return null; +}) + +// CORRECT - generate random in activity, pass to orchestrator +.addActivity("GetRandomDelay", ctx -> { + return new Random().nextInt(10); // OK in activity +}) + +.addOrchestration("GoodOrchestration", ctx -> { + int delay = ctx.callActivity("GetRandomDelay", null, Integer.class).await(); + ctx.createTimer(Duration.ofSeconds(delay)).await(); // Deterministic + return null; +}) +``` + +#### Sleeping/Delays + +```java +// WRONG - Thread.sleep blocks and doesn't persist +.addOrchestration("BadOrchestration", ctx -> { + ctx.callActivity("Step1", null, Void.class).await(); + Thread.sleep(60000); // Non-durable! Lost on restart + ctx.callActivity("Step2", null, Void.class).await(); + return null; +}) + +// CORRECT - ctx.createTimer is durable +.addOrchestration("GoodOrchestration", ctx -> { + ctx.callActivity("Step1", null, Void.class).await(); + ctx.createTimer(Duration.ofMinutes(1)).await(); // Durable timer + ctx.callActivity("Step2", null, Void.class).await(); + return null; +}) +``` + +#### HTTP Calls and I/O + +```java +// WRONG - HTTP call in orchestrator is non-deterministic +.addOrchestration("BadOrchestration", ctx -> { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://api.example.com/data")) + .build(); + HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString()); // Non-deterministic! + return response.body(); +}) + +// CORRECT - move I/O to activity +.addActivity("FetchData", ctx -> { + String url = ctx.getInput(String.class); + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .build(); + HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString()); // OK in activity + return response.body(); +}) + +.addOrchestration("GoodOrchestration", ctx -> { + String data = ctx.callActivity("FetchData", + "https://api.example.com/data", String.class).await(); // Deterministic + return data; +}) +``` + +#### Database Access + +```java +// WRONG - database query in orchestrator +.addOrchestration("BadOrchestration", ctx -> { + Connection conn = DriverManager.getConnection(dbUrl); // Non-deterministic! + PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id=?"); + // ... + return null; +}) + +// CORRECT - database access in activity +.addActivity("GetUser", ctx -> { + String userId = ctx.getInput(String.class); + Connection conn = DriverManager.getConnection(dbUrl); // OK in activity + PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id=?"); + stmt.setString(1, userId); + ResultSet rs = stmt.executeQuery(); + // ... + return user; +}) + +.addOrchestration("GoodOrchestration", ctx -> { + String userId = ctx.getInput(String.class); + User user = ctx.callActivity("GetUser", userId, User.class).await(); + return user; +}) +``` + +#### Environment Variables + +```java +// WRONG - env var might change between replays +.addOrchestration("BadOrchestration", ctx -> { + String apiEndpoint = System.getenv("API_ENDPOINT"); // Could change! + ctx.callActivity("CallApi", apiEndpoint, Void.class).await(); + return null; +}) + +// CORRECT - pass config as input or read in activity +.addOrchestration("GoodOrchestration", ctx -> { + Config config = ctx.getInput(Config.class); + String apiEndpoint = config.getApiEndpoint(); // From input, deterministic + ctx.callActivity("CallApi", apiEndpoint, Void.class).await(); + return null; +}) + +// ALSO CORRECT - read env var in activity +.addActivity("CallApi", ctx -> { + String apiEndpoint = System.getenv("API_ENDPOINT"); // OK in activity + // make the call... + return null; +}) +``` + +#### Collection Iteration Order + +```java +// WRONG - HashMap iteration order is non-deterministic +.addOrchestration("BadOrchestration", ctx -> { + Map items = ctx.getInput(HashMap.class); + for (String key : items.keySet()) { // Order not guaranteed! + ctx.callActivity("Process", key, Void.class).await(); + } + return null; +}) + +// CORRECT - use TreeMap or sorted keys for deterministic order +.addOrchestration("GoodOrchestration", ctx -> { + Map items = ctx.getInput(HashMap.class); + List sortedKeys = new ArrayList<>(items.keySet()); + Collections.sort(sortedKeys); // Guaranteed order + for (String key : sortedKeys) { + ctx.callActivity("Process", key, Void.class).await(); + } + return null; +}) +``` + +### Using await() + +In Java, orchestrator functions use `.await()` to wait for durable operations: + +```java +// CORRECT - use await() to get result +String result = ctx.callActivity("MyActivity", input, String.class).await(); + +// WRONG - forgetting await() returns Task, not result +Task task = ctx.callActivity("MyActivity", input, String.class); // Returns Task! +``` + +### Error Handling + +```java +.addOrchestration("OrchestrationWithErrorHandling", ctx -> { + String input = ctx.getInput(String.class); + try { + String result = ctx.callActivity("RiskyActivity", input, String.class).await(); + return result; + } catch (TaskFailedException ex) { + // Activity failed - implement compensation + ctx.setCustomStatus(Map.of("error", ex.getMessage())); + ctx.callActivity("CompensationActivity", input, Void.class).await(); + return "Compensated"; + } +}) +``` + +### Retry Policies + +```java +TaskOptions options = new TaskOptions(new RetryPolicy( + 3, // maxNumberOfAttempts + Duration.ofSeconds(5), // firstRetryInterval + 2.0, // backoffCoefficient + Duration.ofMinutes(1), // maxRetryInterval + Duration.ofMinutes(5) // retryTimeout +)); + +ctx.callActivity("UnreliableActivity", input, String.class, options).await(); +``` + +## Connection & Authentication + +### Connection String Formats + +```java +// Local emulator (no auth) +"Endpoint=http://localhost:8080;TaskHub=default;Authentication=None" + +// Azure with DefaultAzureCredential +"Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=DefaultAzure" + +// Azure with Managed Identity +"Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=ManagedIdentity" +``` + +### Connection Helper + +```java +public static String getConnectionString() { + String endpoint = System.getenv("ENDPOINT"); + String taskHub = System.getenv("TASKHUB"); + + if (endpoint == null) endpoint = "http://localhost:8080"; + if (taskHub == null) taskHub = "default"; + + String authType = endpoint.startsWith("http://localhost") ? "None" : "DefaultAzure"; + return String.format("Endpoint=%s;TaskHub=%s;Authentication=%s", + endpoint, taskHub, authType); +} +``` + +## Local Development with Emulator + +```bash +# Pull and run the emulator +docker pull mcr.microsoft.com/dts/dts-emulator:latest +docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest + +# Dashboard available at http://localhost:8082 +``` + +## Client Operations + +```java +DurableTaskClient client = new DurableTaskGrpcClientBuilder() + .connectionString(connectionString) + .build(); + +// Schedule new orchestration +String instanceId = client.scheduleNewOrchestrationInstance("MyOrchestration", input); + +// Schedule with custom instance ID +String instanceId = client.scheduleNewOrchestrationInstance( + "MyOrchestration", input, "my-custom-id"); + +// Wait for completion +OrchestrationMetadata result = client.waitForInstanceCompletion( + instanceId, Duration.ofSeconds(60), true); + +// Get current status +OrchestrationMetadata state = client.getInstanceMetadata(instanceId, true); + +// Raise external event +client.raiseEvent(instanceId, "ApprovalEvent", approvalData); + +// Terminate orchestration +client.terminate(instanceId, "User cancelled"); + +// Suspend/Resume +client.suspend(instanceId, "Pausing for maintenance"); +client.resume(instanceId, "Resuming operation"); +``` + +## References + +- **[patterns.md](references/patterns.md)** - Detailed pattern implementations (Fan-Out/Fan-In, Human Interaction, Sub-Orchestrations) +- **[setup.md](references/setup.md)** - Azure Durable Task Scheduler provisioning and deployment diff --git a/.claude/skills/durable-task-java/references/patterns.md b/.claude/skills/durable-task-java/references/patterns.md new file mode 100644 index 0000000..9a4192c --- /dev/null +++ b/.claude/skills/durable-task-java/references/patterns.md @@ -0,0 +1,629 @@ +# Durable Task Java SDK Patterns + +Detailed implementations of workflow patterns for Java applications. + +## Function Chaining + +Sequential activity execution where each step depends on the previous result. + +```java +// Orchestration - process order through sequential steps +.addOrchestration("OrderProcessingWorkflow", ctx -> { + OrderInput input = ctx.getInput(OrderInput.class); + + // Step 1: Validate + ValidationResult validation = ctx.callActivity( + "ValidateOrder", input, ValidationResult.class).await(); + + if (!validation.isValid()) { + return new OrderResult(false, validation.getErrors()); + } + + // Step 2: Calculate pricing + PricingInfo pricing = ctx.callActivity( + "CalculatePricing", input, PricingInfo.class).await(); + + // Step 3: Reserve inventory + ReservationResult reservation = ctx.callActivity( + "ReserveInventory", input.getItems(), ReservationResult.class).await(); + + // Step 4: Process payment + PaymentResult payment = ctx.callActivity( + "ProcessPayment", + new PaymentInput(input.getPaymentInfo(), pricing.getTotal()), + PaymentResult.class).await(); + + // Step 5: Complete order + OrderResult result = ctx.callActivity( + "CompleteOrder", + new CompleteOrderInput(input, reservation, payment), + OrderResult.class).await(); + + return result; +}) + +// Activities +.addActivity("ValidateOrder", ctx -> { + OrderInput input = ctx.getInput(OrderInput.class); + // Validation logic + List errors = new ArrayList<>(); + if (input.getItems().isEmpty()) { + errors.add("Order must contain at least one item"); + } + if (input.getPaymentInfo() == null) { + errors.add("Payment information required"); + } + return new ValidationResult(errors.isEmpty(), errors); +}) + +.addActivity("CalculatePricing", ctx -> { + OrderInput input = ctx.getInput(OrderInput.class); + double subtotal = input.getItems().stream() + .mapToDouble(item -> item.getPrice() * item.getQuantity()) + .sum(); + double tax = subtotal * 0.08; + double shipping = subtotal > 100 ? 0 : 9.99; + return new PricingInfo(subtotal, tax, shipping, subtotal + tax + shipping); +}) +``` + +## Fan-Out/Fan-In (Parallel Processing) + +Execute multiple activities in parallel and aggregate results. + +### Basic Parallel Execution + +```java +.addOrchestration("ParallelProcessing", ctx -> { + List items = ctx.getInput(new TypeReference>() {}); + + // Create tasks for all items + List> tasks = new ArrayList<>(); + for (String item : items) { + Task task = ctx.callActivity( + "ProcessItem", item, ProcessResult.class); + tasks.add(task); + } + + // Wait for all tasks to complete + List results = ctx.allOf(tasks).await(); + + // Aggregate results + int successCount = (int) results.stream() + .filter(ProcessResult::isSuccess) + .count(); + + return new AggregateResult(results.size(), successCount); +}) +``` + +### Batched Parallel Processing (Large Scale) + +```java +.addOrchestration("BatchedParallelProcessing", ctx -> { + List allItems = ctx.getInput(new TypeReference>() {}); + int batchSize = 10; // Process 10 at a time to avoid overload + + List allResults = new ArrayList<>(); + + // Process in batches + for (int i = 0; i < allItems.size(); i += batchSize) { + List batch = allItems.subList( + i, Math.min(i + batchSize, allItems.size())); + + // Create tasks for this batch + List> batchTasks = new ArrayList<>(); + for (WorkItem item : batch) { + batchTasks.add(ctx.callActivity( + "ProcessWorkItem", item, ProcessResult.class)); + } + + // Wait for batch to complete + List batchResults = ctx.allOf(batchTasks).await(); + allResults.addAll(batchResults); + + // Update status after each batch + ctx.setCustomStatus(Map.of( + "processed", i + batch.size(), + "total", allItems.size() + )); + } + + return allResults; +}) +``` + +### Fan-Out with Different Activities + +```java +.addOrchestration("MultiSourceAggregation", ctx -> { + String query = ctx.getInput(String.class); + + // Fan out to multiple data sources in parallel + Task> catalogTask = ctx.callActivity( + "SearchCatalog", query, new TypeReference>() {}); + Task> inventoryTask = ctx.callActivity( + "SearchInventory", query, new TypeReference>() {}); + Task> warehouseTask = ctx.callActivity( + "SearchWarehouse", query, new TypeReference>() {}); + + // Wait for all searches to complete + ctx.allOf(List.of(catalogTask, inventoryTask, warehouseTask)).await(); + + // Combine and deduplicate results + List combined = ctx.callActivity( + "MergeResults", + new MergeInput( + catalogTask.await(), + inventoryTask.await(), + warehouseTask.await() + ), + new TypeReference>() {}).await(); + + return combined; +}) +``` + +## Human Interaction (Approval Workflow) + +Workflow that pauses to wait for human input with timeout support. + +```java +.addOrchestration("ApprovalWorkflow", ctx -> { + ApprovalRequest request = ctx.getInput(ApprovalRequest.class); + Duration approvalTimeout = Duration.ofHours(72); // 3 days + + // Send approval request notification + ctx.callActivity("SendApprovalRequest", request, Void.class).await(); + ctx.setCustomStatus(Map.of("status", "WaitingForApproval", "requestedAt", ctx.getCurrentInstant().toString())); + + // Wait for approval event or timeout + try { + ApprovalResponse response = ctx.waitForExternalEvent( + "ApprovalResponse", approvalTimeout, ApprovalResponse.class).await(); + + if (response.isApproved()) { + // Process approved request + ProcessResult result = ctx.callActivity( + "ProcessApprovedRequest", request, ProcessResult.class).await(); + + ctx.callActivity("SendApprovalNotification", + new NotificationInput(request, "Approved and processed"), Void.class).await(); + + return new WorkflowResult("Approved", result); + } else { + // Handle rejection + ctx.callActivity("SendRejectionNotification", + new RejectionInput(request, response.getReason()), Void.class).await(); + + return new WorkflowResult("Rejected", response.getReason()); + } + + } catch (TaskCanceledException e) { + // Timeout occurred - escalate + ctx.callActivity("EscalateApproval", request, Void.class).await(); + + // Wait for escalation response + try { + ApprovalResponse escalatedResponse = ctx.waitForExternalEvent( + "EscalatedApprovalResponse", Duration.ofHours(24), ApprovalResponse.class).await(); + + if (escalatedResponse.isApproved()) { + return new WorkflowResult("ApprovedAfterEscalation", null); + } else { + return new WorkflowResult("RejectedAfterEscalation", escalatedResponse.getReason()); + } + } catch (TaskCanceledException e2) { + // Final timeout - auto-reject + ctx.callActivity("SendTimeoutNotification", request, Void.class).await(); + return new WorkflowResult("TimedOut", "No response after escalation"); + } + } +}) + +// Activity to send approval request +.addActivity("SendApprovalRequest", ctx -> { + ApprovalRequest request = ctx.getInput(ApprovalRequest.class); + // Send email, Teams message, etc. + // Include link: /api/approval?instanceId=xxx&action=approve + System.out.println("Approval request sent for: " + request.getDescription()); + return null; +}) +``` + +### Raising Approval Event (from external API) + +```java +// In your REST controller or message handler +@PostMapping("/api/approval/{instanceId}") +public ResponseEntity handleApproval( + @PathVariable String instanceId, + @RequestBody ApprovalResponse response) { + + client.raiseEvent(instanceId, "ApprovalResponse", response); + return ResponseEntity.ok("Approval recorded"); +} +``` + +## Sub-Orchestrations + +Compose workflows from reusable orchestration components. + +```java +// Parent orchestration +.addOrchestration("OrderFulfillment", ctx -> { + OrderRequest order = ctx.getInput(OrderRequest.class); + + // Sub-orchestration for payment processing + PaymentResult payment = ctx.callSubOrchestrator( + "PaymentProcessingWorkflow", + order.getPaymentInfo(), + PaymentResult.class).await(); + + if (!payment.isSuccessful()) { + return new FulfillmentResult(false, "Payment failed: " + payment.getError()); + } + + // Sub-orchestration for each shipment (parallel) + List> shipmentTasks = new ArrayList<>(); + for (ShipmentRequest shipment : order.getShipments()) { + shipmentTasks.add(ctx.callSubOrchestrator( + "ShipmentWorkflow", shipment, ShipmentResult.class)); + } + + List shipmentResults = ctx.allOf(shipmentTasks).await(); + + // Sub-orchestration for notification + ctx.callSubOrchestrator( + "NotificationWorkflow", + new NotificationRequest(order, shipmentResults), + Void.class).await(); + + return new FulfillmentResult(true, shipmentResults); +}) + +// Child orchestration - payment processing +.addOrchestration("PaymentProcessingWorkflow", ctx -> { + PaymentInfo payment = ctx.getInput(PaymentInfo.class); + + // Validate card + boolean isValid = ctx.callActivity( + "ValidatePaymentMethod", payment, Boolean.class).await(); + + if (!isValid) { + return new PaymentResult(false, "Invalid payment method"); + } + + // Attempt charge with retry + TaskOptions retryOptions = new TaskOptions(new RetryPolicy( + 3, Duration.ofSeconds(5), 2.0, Duration.ofMinutes(1), null)); + + ChargeResult charge = ctx.callActivity( + "ChargePayment", payment, ChargeResult.class, retryOptions).await(); + + return new PaymentResult(charge.isSuccessful(), charge.getTransactionId()); +}) + +// Child orchestration - shipment +.addOrchestration("ShipmentWorkflow", ctx -> { + ShipmentRequest shipment = ctx.getInput(ShipmentRequest.class); + + // Reserve inventory + ctx.callActivity("ReserveInventory", shipment.getItems(), Void.class).await(); + + // Create shipping label + ShippingLabel label = ctx.callActivity( + "CreateShippingLabel", shipment, ShippingLabel.class).await(); + + // Schedule pickup + PickupConfirmation pickup = ctx.callActivity( + "SchedulePickup", label, PickupConfirmation.class).await(); + + return new ShipmentResult(label.getTrackingNumber(), pickup.getScheduledTime()); +}) +``` + +## Eternal Orchestrations + +Long-running orchestrations that periodically perform work using `continueAsNew`. + +```java +.addOrchestration("PeriodicCleanupWorkflow", ctx -> { + CleanupState state = ctx.getInput(CleanupState.class); + if (state == null) { + state = new CleanupState(0, Instant.EPOCH); + } + + // Perform cleanup work + CleanupResult result = ctx.callActivity( + "PerformCleanup", state, CleanupResult.class).await(); + + // Log completion + ctx.callActivity("LogCleanupResult", result, Void.class).await(); + + // Update status + ctx.setCustomStatus(Map.of( + "lastRun", ctx.getCurrentInstant().toString(), + "totalRuns", state.getRunCount() + 1, + "itemsCleaned", result.getItemsCleaned() + )); + + // Wait until next scheduled time + ctx.createTimer(Duration.ofHours(1)).await(); + + // Continue as new to prevent history buildup + // Pass updated state for the next iteration + CleanupState nextState = new CleanupState( + state.getRunCount() + 1, + ctx.getCurrentInstant() + ); + ctx.continueAsNew(nextState); + + return null; // Never reached due to continueAsNew +}) +``` + +### Graceful Termination for Eternal Orchestrations + +```java +.addOrchestration("EternalWorkflowWithGracefulStop", ctx -> { + WorkflowState state = ctx.getInput(WorkflowState.class); + if (state == null) { + state = new WorkflowState(0, false); + } + + // Check for stop signal + if (state.shouldStop()) { + ctx.callActivity("PerformFinalCleanup", null, Void.class).await(); + return "Workflow stopped gracefully"; + } + + // Do work + ctx.callActivity("PerformPeriodicWork", state, Void.class).await(); + + // Wait for either timer or stop event + Task timerTask = ctx.createTimer(Duration.ofMinutes(5)); + Task stopTask = ctx.waitForExternalEvent( + "StopWorkflow", Duration.ofMinutes(5), Boolean.class); + + Task winner = ctx.anyOf(List.of(timerTask, stopTask)).await(); + + // Check if stop event was raised + boolean shouldStop = false; + try { + // If stopTask completed, get its value + if (stopTask.isDone()) { + shouldStop = stopTask.await(); + } + } catch (Exception e) { + // Timer won or event wasn't raised + } + + // Continue as new + ctx.continueAsNew(new WorkflowState(state.getIteration() + 1, shouldStop)); + return null; +}) +``` + +## Monitoring Pattern + +Periodic polling with configurable timeouts and backoff. + +```java +.addOrchestration("MonitorDeployment", ctx -> { + MonitorConfig config = ctx.getInput(MonitorConfig.class); + + Instant startTime = ctx.getCurrentInstant(); + Duration maxDuration = Duration.ofMinutes(config.getTimeoutMinutes()); + Duration pollingInterval = Duration.ofSeconds(config.getInitialPollingSeconds()); + Duration maxPollingInterval = Duration.ofMinutes(5); + int attempts = 0; + + while (true) { + attempts++; + + // Check deployment status + DeploymentStatus status = ctx.callActivity( + "CheckDeploymentStatus", config.getDeploymentId(), DeploymentStatus.class).await(); + + ctx.setCustomStatus(Map.of( + "status", status.getState(), + "attempts", attempts, + "lastCheck", ctx.getCurrentInstant().toString() + )); + + // Check for terminal states + if (status.isSuccessful()) { + ctx.callActivity("NotifyDeploymentSuccess", config, Void.class).await(); + return new MonitorResult(true, "Deployment succeeded", attempts); + } + + if (status.isFailed()) { + ctx.callActivity("NotifyDeploymentFailure", + new FailureInfo(config, status.getError()), Void.class).await(); + return new MonitorResult(false, "Deployment failed: " + status.getError(), attempts); + } + + // Check for timeout + Duration elapsed = Duration.between(startTime, ctx.getCurrentInstant()); + if (elapsed.compareTo(maxDuration) > 0) { + ctx.callActivity("NotifyDeploymentTimeout", config, Void.class).await(); + return new MonitorResult(false, "Monitoring timed out", attempts); + } + + // Wait before next poll with exponential backoff + ctx.createTimer(pollingInterval).await(); + + // Increase polling interval (exponential backoff with cap) + pollingInterval = pollingInterval.multipliedBy(2); + if (pollingInterval.compareTo(maxPollingInterval) > 0) { + pollingInterval = maxPollingInterval; + } + } +}) +``` + +## Durable Timers and Scheduled Execution + +### Delayed Execution + +```java +.addOrchestration("ScheduledReminder", ctx -> { + ReminderInput input = ctx.getInput(ReminderInput.class); + + // Calculate delay until reminder time + Duration delay = Duration.between(ctx.getCurrentInstant(), input.getReminderTime()); + + if (!delay.isNegative()) { + // Wait until reminder time + ctx.createTimer(delay).await(); + } + + // Send the reminder + ctx.callActivity("SendReminder", input, Void.class).await(); + + return "Reminder sent"; +}) +``` + +### Recurring Schedule + +```java +.addOrchestration("RecurringReport", ctx -> { + ReportConfig config = ctx.getInput(ReportConfig.class); + + // Generate and send report + ReportData report = ctx.callActivity( + "GenerateReport", config, ReportData.class).await(); + ctx.callActivity("SendReport", report, Void.class).await(); + + // Calculate time until next run (e.g., next Monday 9 AM) + Instant nextRun = calculateNextRunTime(ctx.getCurrentInstant(), config.getSchedule()); + Duration delay = Duration.between(ctx.getCurrentInstant(), nextRun); + + // Wait until next scheduled time + ctx.createTimer(delay).await(); + + // Continue as new for the next iteration + ctx.continueAsNew(config); + return null; +}) + +// Helper method (must be deterministic - no external calls) +private static Instant calculateNextRunTime(Instant current, String schedule) { + // Parse cron-like schedule and calculate next run + // This logic must be deterministic + ZonedDateTime now = current.atZone(ZoneOffset.UTC); + ZonedDateTime next = now.plusWeeks(1) + .with(DayOfWeek.MONDAY) + .withHour(9) + .withMinute(0) + .withSecond(0); + return next.toInstant(); +} +``` + +## Saga Pattern (Distributed Transactions) + +Implement compensating transactions for distributed operations. + +```java +.addOrchestration("BookingWorkflow", ctx -> { + BookingRequest request = ctx.getInput(BookingRequest.class); + + String flightReservation = null; + String hotelReservation = null; + String carReservation = null; + + try { + // Step 1: Reserve flight + flightReservation = ctx.callActivity( + "ReserveFlight", request.getFlight(), String.class).await(); + + // Step 2: Reserve hotel + hotelReservation = ctx.callActivity( + "ReserveHotel", request.getHotel(), String.class).await(); + + // Step 3: Reserve car + carReservation = ctx.callActivity( + "ReserveCar", request.getCar(), String.class).await(); + + // All reservations successful + return new BookingResult(true, flightReservation, hotelReservation, carReservation); + + } catch (TaskFailedException e) { + // Compensate in reverse order + List> compensations = new ArrayList<>(); + + if (carReservation != null) { + compensations.add(ctx.callActivity( + "CancelCarReservation", carReservation, Void.class)); + } + if (hotelReservation != null) { + compensations.add(ctx.callActivity( + "CancelHotelReservation", hotelReservation, Void.class)); + } + if (flightReservation != null) { + compensations.add(ctx.callActivity( + "CancelFlightReservation", flightReservation, Void.class)); + } + + if (!compensations.isEmpty()) { + ctx.allOf(compensations).await(); + } + + return new BookingResult(false, "Booking failed: " + e.getMessage()); + } +}) +``` + +## Version-Aware Orchestrations + +Handle orchestration versioning for long-running workflows. + +```java +.addOrchestration("VersionedWorkflow", ctx -> { + VersionedInput input = ctx.getInput(VersionedInput.class); + int version = input.getVersion(); + + // Route based on version + if (version == 1) { + return executeV1(ctx, input); + } else if (version == 2) { + return executeV2(ctx, input); + } else { + return executeLatest(ctx, input); + } +}) + +private static Object executeV1(TaskOrchestrationContext ctx, VersionedInput input) { + // Original workflow logic + return ctx.callActivity("ProcessV1", input, Object.class).await(); +} + +private static Object executeV2(TaskOrchestrationContext ctx, VersionedInput input) { + // Updated workflow logic with new step + Object intermediate = ctx.callActivity("ProcessV2Step1", input, Object.class).await(); + return ctx.callActivity("ProcessV2Step2", intermediate, Object.class).await(); +} +``` + +## Type References for Generic Types + +When working with generic types like `List` or `Map`: + +```java +import com.microsoft.durabletask.TypeReference; + +// For List types +List items = ctx.callActivity( + "GetItems", null, new TypeReference>() {}).await(); + +// For Map types +Map counts = ctx.callActivity( + "GetCounts", null, new TypeReference>() {}).await(); + +// For custom generic types +PagedResult customers = ctx.callActivity( + "GetCustomers", page, new TypeReference>() {}).await(); +``` diff --git a/.claude/skills/durable-task-java/references/setup.md b/.claude/skills/durable-task-java/references/setup.md new file mode 100644 index 0000000..d113e54 --- /dev/null +++ b/.claude/skills/durable-task-java/references/setup.md @@ -0,0 +1,837 @@ +# Durable Task Java SDK Setup and Deployment + +Comprehensive setup guide for Azure Durable Task Scheduler with Java applications. + +## Local Development with Docker Emulator + +### Quick Start + +```bash +# Pull the emulator image +docker pull mcr.microsoft.com/dts/dts-emulator:latest + +# Run the emulator +docker run -d \ + -p 8080:8080 \ + -p 8082:8082 \ + --name dts-emulator \ + mcr.microsoft.com/dts/dts-emulator:latest + +# Dashboard available at http://localhost:8082 +``` + +### Docker Compose Setup + +```yaml +# docker-compose.yml +version: '3.8' + +services: + dts-emulator: + image: mcr.microsoft.com/dts/dts-emulator:latest + ports: + - "8080:8080" # gRPC endpoint + - "8082:8082" # Dashboard + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:8082"] + interval: 5s + timeout: 3s + retries: 3 + + worker: + build: . + depends_on: + dts-emulator: + condition: service_healthy + environment: + - DURABLE_TASK_CONNECTION_STRING=Endpoint=http://dts-emulator:8080;TaskHub=default;Authentication=None +``` + +### Multi-Hub Development + +```yaml +# docker-compose-multi-hub.yml +version: '3.8' + +services: + dts-emulator: + image: mcr.microsoft.com/dts/dts-emulator:latest + ports: + - "8080:8080" + - "8082:8082" + + order-worker: + build: ./order-service + environment: + - DURABLE_TASK_CONNECTION_STRING=Endpoint=http://dts-emulator:8080;TaskHub=orders;Authentication=None + + notification-worker: + build: ./notification-service + environment: + - DURABLE_TASK_CONNECTION_STRING=Endpoint=http://dts-emulator:8080;TaskHub=notifications;Authentication=None +``` + +## Azure Durable Task Scheduler Provisioning + +### Prerequisites + +```bash +# Install/update Azure CLI +brew install azure-cli # macOS +# or +curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash # Linux + +# Login to Azure +az login + +# Set subscription +az account set --subscription "your-subscription-id" +``` + +### Create Durable Task Scheduler + +```bash +# Variables +RESOURCE_GROUP="my-dts-rg" +LOCATION="eastus" +SCHEDULER_NAME="my-dts-scheduler" +TASKHUB_NAME="my-taskhub" + +# Create resource group +az group create --name $RESOURCE_GROUP --location $LOCATION + +# Create Durable Task Scheduler namespace +az durabletask scheduler create \ + --name $SCHEDULER_NAME \ + --resource-group $RESOURCE_GROUP \ + --location $LOCATION \ + --sku "standard" + +# Create a Task Hub +az durabletask taskhub create \ + --scheduler-name $SCHEDULER_NAME \ + --resource-group $RESOURCE_GROUP \ + --name $TASKHUB_NAME + +# Get the endpoint +ENDPOINT=$(az durabletask scheduler show \ + --name $SCHEDULER_NAME \ + --resource-group $RESOURCE_GROUP \ + --query "properties.endpoint" -o tsv) + +echo "Connection String: Endpoint=$ENDPOINT;TaskHub=$TASKHUB_NAME;Authentication=DefaultAzure" +``` + +### Bicep Template + +```bicep +// main.bicep +@description('Name of the Durable Task Scheduler') +param schedulerName string + +@description('Location for resources') +param location string = resourceGroup().location + +@description('Task Hub name') +param taskHubName string = 'default' + +@description('SKU for the scheduler') +@allowed(['basic', 'standard', 'premium']) +param sku string = 'standard' + +resource scheduler 'Microsoft.DurableTask/schedulers@2024-10-01-preview' = { + name: schedulerName + location: location + properties: { + sku: { + name: sku + } + } +} + +resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2024-10-01-preview' = { + parent: scheduler + name: taskHubName + properties: {} +} + +output endpoint string = scheduler.properties.endpoint +output connectionString string = 'Endpoint=${scheduler.properties.endpoint};TaskHub=${taskHubName};Authentication=DefaultAzure' +``` + +Deploy with: +```bash +az deployment group create \ + --resource-group $RESOURCE_GROUP \ + --template-file main.bicep \ + --parameters schedulerName=$SCHEDULER_NAME taskHubName=$TASKHUB_NAME +``` + +## Authentication Configuration + +### DefaultAzureCredential (Recommended) + +Works locally with Azure CLI credentials and in Azure with Managed Identity: + +```java +String connectionString = "Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=DefaultAzure"; + +DurableTaskGrpcWorker worker = new DurableTaskGrpcWorkerBuilder() + .connectionString(connectionString) + .addOrchestration("MyOrchestration", ctx -> { /* ... */ }) + .build(); +``` + +Dependencies required: +```xml + + com.azure + azure-identity + 1.11.0 + +``` + +### Managed Identity + +```java +// System-assigned managed identity +String connectionString = "Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=ManagedIdentity"; + +// User-assigned managed identity +String connectionString = "Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=ManagedIdentity;ClientId="; +``` + +### Azure CLI (Local Development) + +```bash +# Login to Azure CLI +az login + +# Your Java app will automatically use Azure CLI credentials +# with Authentication=DefaultAzure +``` + +### Role Assignments + +Grant the worker/client identity the `Durable Task Data Owner` role: + +```bash +# Get the scheduler resource ID +SCHEDULER_ID=$(az durabletask scheduler show \ + --name $SCHEDULER_NAME \ + --resource-group $RESOURCE_GROUP \ + --query id -o tsv) + +# Assign role to managed identity +az role assignment create \ + --assignee "" \ + --role "Durable Task Data Owner" \ + --scope $SCHEDULER_ID +``` + +## Application Integration + +### Console Application + +```java +// Main.java +import com.microsoft.durabletask.*; +import com.microsoft.durabletask.azuremanaged.*; +import java.time.Duration; + +public class Main { + public static void main(String[] args) throws Exception { + String connectionString = getConnectionString(); + + // Create worker + DurableTaskGrpcWorker worker = new DurableTaskGrpcWorkerBuilder() + .connectionString(connectionString) + .addOrchestration("ProcessOrder", Orchestrations::processOrder) + .addActivity("ValidateOrder", Activities::validateOrder) + .addActivity("ProcessPayment", Activities::processPayment) + .addActivity("SendConfirmation", Activities::sendConfirmation) + .build(); + + // Start worker in background thread + Thread workerThread = new Thread(() -> { + try { + worker.start(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + workerThread.start(); + + // Create client + DurableTaskClient client = new DurableTaskGrpcClientBuilder() + .connectionString(connectionString) + .build(); + + // Schedule orchestration + OrderInput input = new OrderInput("order-123", 99.99); + String instanceId = client.scheduleNewOrchestrationInstance("ProcessOrder", input); + System.out.println("Started: " + instanceId); + + // Wait for result + OrchestrationMetadata result = client.waitForInstanceCompletion( + instanceId, Duration.ofMinutes(5), true); + + System.out.println("Status: " + result.getRuntimeStatus()); + System.out.println("Output: " + result.readOutputAs(String.class)); + + // Cleanup + worker.close(); + client.close(); + } + + private static String getConnectionString() { + String cs = System.getenv("DURABLE_TASK_CONNECTION_STRING"); + return cs != null ? cs : "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"; + } +} +``` + +### Spring Boot Integration + +```java +// DurableTaskConfig.java +import com.microsoft.durabletask.*; +import com.microsoft.durabletask.azuremanaged.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.*; + +@Configuration +public class DurableTaskConfig { + + @Value("${durable-task.connection-string}") + private String connectionString; + + @Bean + public DurableTaskClient durableTaskClient() { + return new DurableTaskGrpcClientBuilder() + .connectionString(connectionString) + .build(); + } + + @Bean + public DurableTaskGrpcWorker durableTaskWorker( + List orchestrations, + List activities) { + + DurableTaskGrpcWorkerBuilder builder = new DurableTaskGrpcWorkerBuilder() + .connectionString(connectionString); + + for (OrchestrationDefinition orch : orchestrations) { + builder.addOrchestration(orch.getName(), orch.getImplementation()); + } + + for (ActivityDefinition act : activities) { + builder.addActivity(act.getName(), act.getImplementation()); + } + + return builder.build(); + } +} + +// WorkerLifecycle.java +import org.springframework.stereotype.Component; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; + +@Component +public class WorkerLifecycle { + + private final DurableTaskGrpcWorker worker; + private Thread workerThread; + + public WorkerLifecycle(DurableTaskGrpcWorker worker) { + this.worker = worker; + } + + @PostConstruct + public void start() { + workerThread = new Thread(() -> { + try { + worker.start(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + workerThread.setDaemon(true); + workerThread.start(); + } + + @PreDestroy + public void stop() { + try { + worker.close(); + } catch (Exception e) { + // Log error + } + } +} + +// WorkflowController.java +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/workflows") +public class WorkflowController { + + private final DurableTaskClient client; + + public WorkflowController(DurableTaskClient client) { + this.client = client; + } + + @PostMapping("/orders") + public WorkflowResponse startOrder(@RequestBody OrderInput input) { + String instanceId = client.scheduleNewOrchestrationInstance("ProcessOrder", input); + return new WorkflowResponse(instanceId, "Started"); + } + + @GetMapping("/{instanceId}") + public WorkflowStatus getStatus(@PathVariable String instanceId) throws Exception { + OrchestrationMetadata metadata = client.getInstanceMetadata(instanceId, true); + return new WorkflowStatus( + instanceId, + metadata.getRuntimeStatus().toString(), + metadata.getCustomStatus() + ); + } + + @PostMapping("/{instanceId}/events/{eventName}") + public void raiseEvent( + @PathVariable String instanceId, + @PathVariable String eventName, + @RequestBody Object eventData) { + client.raiseEvent(instanceId, eventName, eventData); + } +} +``` + +### application.yml for Spring Boot + +```yaml +durable-task: + connection-string: ${DURABLE_TASK_CONNECTION_STRING:Endpoint=http://localhost:8080;TaskHub=default;Authentication=None} +``` + +## Deployment Options + +### Container Apps + +```yaml +# container-app.yaml +apiVersion: apps/v1 +kind: ContainerApp +metadata: + name: dts-worker +spec: + template: + containers: + - name: worker + image: myregistry.azurecr.io/my-worker:latest + env: + - name: DURABLE_TASK_CONNECTION_STRING + secretRef: dts-connection-string + resources: + cpu: 0.5 + memory: 1Gi + scale: + minReplicas: 1 + maxReplicas: 10 + rules: + - name: queue-scaling + custom: + type: external + metadata: + scalerAddress: "azure-scheduler-scaler:5050" +``` + +### Kubernetes Deployment + +```yaml +# deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dts-worker +spec: + replicas: 3 + selector: + matchLabels: + app: dts-worker + template: + metadata: + labels: + app: dts-worker + spec: + serviceAccountName: dts-worker-sa + containers: + - name: worker + image: myregistry.azurecr.io/my-worker:latest + env: + - name: DURABLE_TASK_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: dts-secrets + key: connection-string + - name: AZURE_CLIENT_ID # For workload identity + valueFrom: + secretKeyRef: + name: dts-secrets + key: client-id + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: 1000m + memory: 1Gi + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 +``` + +### Dockerfile + +```dockerfile +# Dockerfile +FROM eclipse-temurin:17-jdk as builder + +WORKDIR /app +COPY pom.xml . +COPY src ./src + +RUN apt-get update && apt-get install -y maven +RUN mvn clean package -DskipTests + +FROM eclipse-temurin:17-jre + +WORKDIR /app +COPY --from=builder /app/target/*.jar app.jar + +ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" + +EXPOSE 8080 +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] +``` + +## Logging and Monitoring + +### SLF4J Configuration + +```xml + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + +``` + +### Application Insights Integration + +```xml + + com.microsoft.azure + applicationinsights-runtime-attach + 3.4.18 + +``` + +```java +// Enable in main method +import com.microsoft.applicationinsights.attach.ApplicationInsights; + +public class Main { + public static void main(String[] args) { + ApplicationInsights.attach(); + // ... rest of application + } +} +``` + +### Health Check Endpoint + +```java +// For containerized deployments, add health endpoints +import com.sun.net.httpserver.*; +import java.io.*; +import java.net.*; + +public class HealthServer { + private final HttpServer server; + + public HealthServer(int port) throws IOException { + server = HttpServer.create(new InetSocketAddress(port), 0); + + server.createContext("/health", exchange -> { + String response = "{\"status\":\"healthy\"}"; + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, response.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes()); + } + }); + + server.createContext("/ready", exchange -> { + String response = "{\"status\":\"ready\"}"; + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, response.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes()); + } + }); + + server.setExecutor(null); + } + + public void start() { + server.start(); + } + + public void stop() { + server.stop(0); + } +} +``` + +## Maven Project Template + +### pom.xml + +```xml + + + 4.0.0 + + com.example + durable-task-worker + 1.0.0 + jar + + + 17 + 17 + UTF-8 + 1.6.2 + + + + + + com.microsoft.durabletask + durabletask-client + ${durabletask.version} + + + com.microsoft.durabletask + durabletask-azuremanaged + ${durabletask.version} + + + + + com.azure + azure-identity + 1.11.0 + + + + + org.slf4j + slf4j-api + 2.0.9 + + + ch.qos.logback + logback-classic + 1.4.11 + + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + com.example.Main + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + false + + + + + + + +``` + +## Testing + +### Unit Testing Orchestrations + +```java +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class OrchestrationTests { + + @Test + void testOrderWorkflowSuccess() { + // Create mock context + TaskOrchestrationContext ctx = mock(TaskOrchestrationContext.class); + + // Setup input + OrderInput input = new OrderInput("order-123", 99.99); + when(ctx.getInput(OrderInput.class)).thenReturn(input); + + // Setup activity calls + when(ctx.callActivity(eq("ValidateOrder"), any(), eq(Boolean.class))) + .thenReturn(completedTask(true)); + when(ctx.callActivity(eq("ProcessPayment"), any(), eq(PaymentResult.class))) + .thenReturn(completedTask(new PaymentResult(true, "tx-123"))); + when(ctx.callActivity(eq("SendConfirmation"), any(), eq(Void.class))) + .thenReturn(completedTask(null)); + + // Execute orchestration + Object result = Orchestrations.processOrder(ctx); + + // Verify + assertNotNull(result); + verify(ctx).callActivity(eq("ValidateOrder"), any(), eq(Boolean.class)); + verify(ctx).callActivity(eq("ProcessPayment"), any(), eq(PaymentResult.class)); + verify(ctx).callActivity(eq("SendConfirmation"), any(), eq(Void.class)); + } + + private Task completedTask(T value) { + Task task = mock(Task.class); + when(task.await()).thenReturn(value); + return task; + } +} +``` + +### Integration Testing with Emulator + +```java +import org.junit.jupiter.api.*; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +class IntegrationTests { + + @Container + static GenericContainer emulator = new GenericContainer<>("mcr.microsoft.com/dts/dts-emulator:latest") + .withExposedPorts(8080, 8082); + + private DurableTaskClient client; + private DurableTaskGrpcWorker worker; + + @BeforeEach + void setup() { + String connectionString = String.format( + "Endpoint=http://%s:%d;TaskHub=test;Authentication=None", + emulator.getHost(), + emulator.getMappedPort(8080) + ); + + worker = new DurableTaskGrpcWorkerBuilder() + .connectionString(connectionString) + .addOrchestration("TestOrchestration", ctx -> { + String input = ctx.getInput(String.class); + return "Hello, " + input + "!"; + }) + .build(); + + new Thread(() -> { + try { worker.start(); } catch (Exception e) {} + }).start(); + + client = new DurableTaskGrpcClientBuilder() + .connectionString(connectionString) + .build(); + } + + @AfterEach + void teardown() throws Exception { + worker.close(); + client.close(); + } + + @Test + void testSimpleOrchestration() throws Exception { + String instanceId = client.scheduleNewOrchestrationInstance("TestOrchestration", "World"); + + OrchestrationMetadata result = client.waitForInstanceCompletion( + instanceId, Duration.ofSeconds(30), true); + + assertEquals(OrchestrationRuntimeStatus.COMPLETED, result.getRuntimeStatus()); + assertEquals("Hello, World!", result.readOutputAs(String.class)); + } +} +``` diff --git a/.claude/skills/durable-task-python/SKILL.md b/.claude/skills/durable-task-python/SKILL.md new file mode 100644 index 0000000..1edce16 --- /dev/null +++ b/.claude/skills/durable-task-python/SKILL.md @@ -0,0 +1,484 @@ +--- +name: durable-task-python +description: Build durable, fault-tolerant workflows in Python using the Durable Task SDK with Azure Durable Task Scheduler. Use when creating orchestrations, activities, entities, or implementing patterns like function chaining, fan-out/fan-in, human interaction, or stateful agents. Applies to any Python application requiring durable execution, state persistence, or distributed transactions without Azure Functions dependency. +--- + +# Durable Task Python SDK with Durable Task Scheduler + +Build fault-tolerant, stateful workflows in Python applications using the Durable Task SDK connected to Azure Durable Task Scheduler. + +## Quick Start + +### Required Packages + +```bash +pip install durabletask durabletask-azuremanaged azure-identity +``` + +Or add to `requirements.txt`: + +```text +durabletask +durabletask-azuremanaged +azure-identity +``` + +### Minimal Worker + Client Setup + +```python +import os +from azure.identity import DefaultAzureCredential +from durabletask import client, task +from durabletask.azuremanaged.client import DurableTaskSchedulerClient +from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker + + +# Activity function +def hello(ctx: task.ActivityContext, name: str) -> str: + return f"Hello {name}!" + + +# Orchestrator function +def my_orchestration(ctx: task.OrchestrationContext, input: str): + result = yield ctx.call_activity(hello, input=input) + return result + + +# Configuration - defaults to local emulator +taskhub = os.getenv("TASKHUB", "default") +endpoint = os.getenv("ENDPOINT", "http://localhost:8080") +secure_channel = endpoint != "http://localhost:8080" +credential = None if endpoint == "http://localhost:8080" else DefaultAzureCredential() + +# Start worker and run orchestration +with DurableTaskSchedulerWorker( + host_address=endpoint, + secure_channel=secure_channel, + taskhub=taskhub, + token_credential=credential +) as worker: + worker.add_orchestrator(my_orchestration) + worker.add_activity(hello) + worker.start() + + # Create client and schedule orchestration + client = DurableTaskSchedulerClient( + host_address=endpoint, + secure_channel=secure_channel, + taskhub=taskhub, + token_credential=credential + ) + + instance_id = client.schedule_new_orchestration(my_orchestration, input="World") + state = client.wait_for_orchestration_completion(instance_id, timeout=60) + + if state and state.runtime_status == client.OrchestrationStatus.COMPLETED: + print(f"Result: {state.serialized_output}") +``` + +## Pattern Selection Guide + +| Pattern | Use When | +|---------|----------| +| **Function Chaining** | Sequential steps where each depends on the previous | +| **Fan-Out/Fan-In** | Parallel processing with aggregated results | +| **Human Interaction** | Workflow pauses for external input/approval | +| **Durable Entities** | Stateful objects with operations (counters, accounts) | +| **Sub-Orchestrations** | Reusable workflow components or version isolation | +| **Eternal Orchestrations** | Long-running background processes with `continue_as_new` | +| **Monitoring** | Periodic polling with configurable timeouts | + +See [references/patterns.md](references/patterns.md) for detailed implementations. + +## Orchestration Structure + +### Basic Orchestrator + +```python +def my_orchestration(ctx: task.OrchestrationContext, input: str): + """Orchestrator function - MUST be deterministic""" + # Call activities sequentially + step1 = yield ctx.call_activity(step1_activity, input=input) + step2 = yield ctx.call_activity(step2_activity, input=step1) + return step2 +``` + +### Basic Activity + +```python +def my_activity(ctx: task.ActivityContext, input: str) -> str: + """Activity function - can have side effects, I/O, non-determinism""" + # Perform actual work here + print(f"Processing: {input}") + return f"Processed: {input}" +``` + +### Registering with Worker + +```python +with DurableTaskSchedulerWorker(...) as worker: + worker.add_orchestrator(my_orchestration) + worker.add_activity(step1_activity) + worker.add_activity(step2_activity) + worker.start() +``` + +## Critical Rules + +### Orchestration Determinism + +Orchestrations replay from history - all code MUST be deterministic. When an orchestration resumes, it replays all previous code to rebuild state. Non-deterministic code produces different results on replay, causing failures. + +**NEVER do inside orchestrations:** +- `datetime.now()`, `datetime.utcnow()` → Use `ctx.current_utc_datetime` +- `uuid.uuid4()` → Use `ctx.new_uuid()` +- `random.random()` → Pass random values from activities +- Direct I/O, HTTP calls, database access → Move to activities +- `time.sleep()`, `asyncio.sleep()` → Use `ctx.create_timer()` +- Environment variables that may change → Pass as input or use activities +- Global mutable state → Pass state through activity results + +**ALWAYS use:** +- `yield ctx.call_activity()` - Call activities +- `yield ctx.call_sub_orchestrator()` - Call sub-orchestrations +- `yield ctx.create_timer()` - Durable delays +- `yield ctx.wait_for_external_event()` - Wait for events +- `ctx.current_utc_datetime` - Current time +- `ctx.new_uuid()` - Generate GUIDs +- `ctx.set_custom_status()` - Set status + +### Non-Determinism Patterns (WRONG vs CORRECT) + +#### Getting Current Time + +```python +# WRONG - datetime.now() returns different value on replay +def bad_orchestration(ctx: task.OrchestrationContext, _): + current_time = datetime.now() # Non-deterministic! + if current_time.hour < 12: + yield ctx.call_activity(morning_activity) + +# CORRECT - ctx.current_utc_datetime is replayed consistently +def good_orchestration(ctx: task.OrchestrationContext, _): + current_time = ctx.current_utc_datetime # Deterministic + if current_time.hour < 12: + yield ctx.call_activity(morning_activity) +``` + +#### Generating UUIDs/Random Values + +```python +# WRONG - uuid4() generates different value on replay +def bad_orchestration(ctx: task.OrchestrationContext, _): + order_id = str(uuid.uuid4()) # Non-deterministic! + yield ctx.call_activity(create_order, input=order_id) + +# CORRECT - ctx.new_uuid() replays the same value +def good_orchestration(ctx: task.OrchestrationContext, _): + order_id = str(ctx.new_uuid()) # Deterministic + yield ctx.call_activity(create_order, input=order_id) +``` + +#### Random Numbers + +```python +# WRONG - random produces different values on replay +def bad_orchestration(ctx: task.OrchestrationContext, _): + delay = random.randint(1, 10) # Non-deterministic! + yield ctx.create_timer(timedelta(seconds=delay)) + +# CORRECT - generate random in activity, pass to orchestrator +def get_random_delay(ctx: task.ActivityContext, _) -> int: + return random.randint(1, 10) # OK in activity + +def good_orchestration(ctx: task.OrchestrationContext, _): + delay = yield ctx.call_activity(get_random_delay) # Deterministic + yield ctx.create_timer(timedelta(seconds=delay)) +``` + +#### Sleeping/Delays + +```python +# WRONG - time.sleep blocks and doesn't persist +def bad_orchestration(ctx: task.OrchestrationContext, _): + yield ctx.call_activity(step1) + time.sleep(60) # Non-durable! Lost on restart + yield ctx.call_activity(step2) + +# CORRECT - ctx.create_timer is durable +def good_orchestration(ctx: task.OrchestrationContext, _): + yield ctx.call_activity(step1) + yield ctx.create_timer(timedelta(seconds=60)) # Durable timer + yield ctx.call_activity(step2) +``` + +#### HTTP Calls and I/O + +```python +# WRONG - HTTP call in orchestrator is non-deterministic +def bad_orchestration(ctx: task.OrchestrationContext, url: str): + import requests + response = requests.get(url) # Non-deterministic! + return response.json() + +# CORRECT - move I/O to activity +def fetch_data(ctx: task.ActivityContext, url: str) -> dict: + import requests + response = requests.get(url) # OK in activity + return response.json() + +def good_orchestration(ctx: task.OrchestrationContext, url: str): + data = yield ctx.call_activity(fetch_data, input=url) # Deterministic + return data +``` + +#### Database Access + +```python +# WRONG - database query in orchestrator +def bad_orchestration(ctx: task.OrchestrationContext, user_id: str): + import sqlite3 + conn = sqlite3.connect('db.sqlite') # Non-deterministic! + cursor = conn.execute("SELECT * FROM users WHERE id=?", (user_id,)) + user = cursor.fetchone() + # ... + +# CORRECT - database access in activity +def get_user(ctx: task.ActivityContext, user_id: str) -> dict: + import sqlite3 + conn = sqlite3.connect('db.sqlite') # OK in activity + cursor = conn.execute("SELECT * FROM users WHERE id=?", (user_id,)) + return dict(cursor.fetchone()) + +def good_orchestration(ctx: task.OrchestrationContext, user_id: str): + user = yield ctx.call_activity(get_user, input=user_id) + # ... +``` + +#### Environment Variables + +```python +# WRONG - env var might change between replays +def bad_orchestration(ctx: task.OrchestrationContext, _): + api_endpoint = os.getenv("API_ENDPOINT") # Could change! + yield ctx.call_activity(call_api, input=api_endpoint) + +# CORRECT - pass config as input or read in activity +def good_orchestration(ctx: task.OrchestrationContext, config: dict): + api_endpoint = config["api_endpoint"] # From input, deterministic + yield ctx.call_activity(call_api, input=api_endpoint) + +# ALSO CORRECT - read env var in activity +def call_api(ctx: task.ActivityContext, _) -> str: + api_endpoint = os.getenv("API_ENDPOINT") # OK in activity + # make the call... +``` + +#### Conditional Logic Based on External State + +```python +# WRONG - file existence can change between replays +def bad_orchestration(ctx: task.OrchestrationContext, path: str): + if os.path.exists(path): # Non-deterministic! + yield ctx.call_activity(process_file, input=path) + +# CORRECT - check in activity +def check_file_exists(ctx: task.ActivityContext, path: str) -> bool: + return os.path.exists(path) # OK in activity + +def good_orchestration(ctx: task.OrchestrationContext, path: str): + exists = yield ctx.call_activity(check_file_exists, input=path) + if exists: # Deterministic - based on activity result + yield ctx.call_activity(process_file, input=path) +``` + +#### Dictionary/Set Iteration Order + +```python +# POTENTIALLY WRONG - dict iteration order may vary (Python < 3.7) +def risky_orchestration(ctx: task.OrchestrationContext, items: dict): + for key in items: # Order might not be guaranteed + yield ctx.call_activity(process, input=key) + +# CORRECT - use sorted keys for deterministic order +def good_orchestration(ctx: task.OrchestrationContext, items: dict): + for key in sorted(items.keys()): # Guaranteed order + yield ctx.call_activity(process, input=key) +``` + +#### Thread-Local or Global State + +```python +# WRONG - global state can change +counter = 0 + +def bad_orchestration(ctx: task.OrchestrationContext, _): + global counter + counter += 1 # Non-deterministic across replays! + yield ctx.call_activity(process, input=counter) + +# CORRECT - pass state through orchestration input/output +def good_orchestration(ctx: task.OrchestrationContext, counter: int): + counter += 1 # Local variable, deterministic + yield ctx.call_activity(process, input=counter) + # If continuing, pass counter forward + ctx.continue_as_new(counter) +``` + +### Using yield + +In Python, orchestrator functions use `yield` to await durable operations: + +```python +# CORRECT - use yield +result = yield ctx.call_activity(my_activity, input="data") + +# WRONG - will not work +result = ctx.call_activity(my_activity, input="data") # Missing yield! +``` + +### Error Handling + +```python +def orchestrator_with_error_handling(ctx: task.OrchestrationContext, input: str): + try: + result = yield ctx.call_activity(risky_activity, input=input) + return result + except task.TaskFailedError as e: + # Activity failed - implement compensation + ctx.set_custom_status({"error": str(e)}) + yield ctx.call_activity(compensation_activity, input=input) + return "Compensated" +``` + +### Retry Policies + +```python +from durabletask.task import RetryPolicy + +retry_policy = RetryPolicy( + first_retry_interval=5, # seconds + max_number_of_attempts=3, + backoff_coefficient=2.0, + max_retry_interval=60, # seconds + retry_timeout=300 # seconds +) + +def orchestrator(ctx: task.OrchestrationContext, _): + result = yield ctx.call_activity( + unreliable_activity, + input="data", + retry_policy=retry_policy + ) + return result +``` + +## Working with Custom Types + +The SDK supports dataclasses, namedtuples, and custom classes: + +```python +from dataclasses import dataclass + +@dataclass +class Order: + product: str + quantity: int + cost: float + +def process_order(ctx: task.ActivityContext, order: Order) -> str: + return f"Processed {order.quantity}x {order.product}" + +def order_workflow(ctx: task.OrchestrationContext, order: Order): + result = yield ctx.call_activity(process_order, input=order) + return result +``` + +## Connection & Authentication + +### Local Emulator (Default) + +```python +# No authentication required +taskhub = "default" +endpoint = "http://localhost:8080" +credential = None +secure_channel = False +``` + +### Azure with DefaultAzureCredential + +```python +from azure.identity import DefaultAzureCredential + +taskhub = "my-taskhub" +endpoint = "https://my-scheduler.region.durabletask.io" +credential = DefaultAzureCredential() +secure_channel = True +``` + +### Authentication Helper + +```python +def get_connection_config(): + endpoint = os.getenv("ENDPOINT", "http://localhost:8080") + taskhub = os.getenv("TASKHUB", "default") + + is_local = endpoint == "http://localhost:8080" + + return { + "host_address": endpoint, + "taskhub": taskhub, + "secure_channel": not is_local, + "token_credential": None if is_local else DefaultAzureCredential() + } + +config = get_connection_config() +worker = DurableTaskSchedulerWorker(**config) +client = DurableTaskSchedulerClient(**config) +``` + +## Local Development with Emulator + +```bash +# Pull and run the emulator +docker pull mcr.microsoft.com/dts/dts-emulator:latest +docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest + +# Dashboard available at http://localhost:8082 +``` + +## Client Operations + +```python +# Schedule new orchestration +instance_id = client.schedule_new_orchestration(my_orchestration, input="data") + +# Schedule with custom instance ID +instance_id = client.schedule_new_orchestration( + my_orchestration, + input="data", + instance_id="my-custom-id" +) + +# Wait for completion +state = client.wait_for_orchestration_completion(instance_id, timeout=60) + +# Get current status +state = client.get_orchestration_state(instance_id) + +# Raise external event +client.raise_orchestration_event(instance_id, "approval_received", data=approval_data) + +# Terminate orchestration +client.terminate_orchestration(instance_id, reason="User cancelled") + +# Suspend/Resume +client.suspend_orchestration(instance_id, reason="Pausing for maintenance") +client.resume_orchestration(instance_id, reason="Resuming operation") +``` + +## References + +- **[patterns.md](references/patterns.md)** - Detailed pattern implementations (Fan-Out/Fan-In, Human Interaction, Entities, Sub-Orchestrations) +- **[setup.md](references/setup.md)** - Azure Durable Task Scheduler provisioning and deployment diff --git a/.claude/skills/durable-task-python/references/patterns.md b/.claude/skills/durable-task-python/references/patterns.md new file mode 100644 index 0000000..87ee827 --- /dev/null +++ b/.claude/skills/durable-task-python/references/patterns.md @@ -0,0 +1,416 @@ +# Durable Task Python Patterns + +Detailed implementation patterns for the Durable Task Python SDK. + +## Function Chaining + +Sequential execution where each step depends on the previous: + +```python +def hello(ctx: task.ActivityContext, name: str) -> str: + """Activity function that returns a greeting""" + return f'Hello {name}!' + + +def chained_workflow(ctx: task.OrchestrationContext, _): + """Orchestrator that chains activity calls sequentially""" + result1 = yield ctx.call_activity(hello, input='Tokyo') + result2 = yield ctx.call_activity(hello, input='Seattle') + result3 = yield ctx.call_activity(hello, input='London') + return [result1, result2, result3] + + +# Registration +worker.add_orchestrator(chained_workflow) +worker.add_activity(hello) +``` + +## Fan-Out/Fan-In + +Parallel processing with aggregated results: + +```python +import random +from durabletask import task + + +def get_work_items(ctx: task.ActivityContext, _) -> list[str]: + """Activity that returns a list of work items""" + count = random.randint(2, 10) + return [f'work item {i}' for i in range(count)] + + +def process_work_item(ctx: task.ActivityContext, item: str) -> int: + """Activity that processes a single work item""" + return random.randint(0, 10) + + +def fanout_fanin_workflow(ctx: task.OrchestrationContext, _): + """Orchestrator that fans out work and fans in results""" + # Get work items + work_items: list[str] = yield ctx.call_activity(get_work_items) + + # Fan-out: schedule all work items in parallel + tasks = [ctx.call_activity(process_work_item, input=item) for item in work_items] + + # Fan-in: wait for all to complete + results: list[int] = yield task.when_all(tasks) + + # Return aggregated results + return { + 'work_items': work_items, + 'results': results, + 'total': sum(results) + } + + +# Registration +worker.add_orchestrator(fanout_fanin_workflow) +worker.add_activity(get_work_items) +worker.add_activity(process_work_item) +``` + +### Batched Fan-Out (Large Scale) + +For large numbers of items, process in batches: + +```python +def batched_fanout_workflow(ctx: task.OrchestrationContext, _): + """Process work items in batches to control parallelism""" + work_items = yield ctx.call_activity(get_work_items) + + batch_size = 10 + all_results = [] + + for i in range(0, len(work_items), batch_size): + batch = work_items[i:i + batch_size] + tasks = [ctx.call_activity(process_work_item, input=item) for item in batch] + batch_results = yield task.when_all(tasks) + all_results.extend(batch_results) + + return {'total': sum(all_results)} +``` + +## Human Interaction + +Workflow that waits for external approval with timeout: + +```python +from collections import namedtuple +from dataclasses import dataclass +from datetime import timedelta +from durabletask import task + + +@dataclass +class Order: + cost: float + product: str + quantity: int + + +def send_approval_request(ctx: task.ActivityContext, order: Order) -> None: + """Send notification requesting approval""" + print(f"Approval needed for {order.product} (${order.cost})") + + +def place_order(ctx: task.ActivityContext, order: Order) -> str: + """Place the order after approval""" + return f"Order placed: {order.quantity}x {order.product}" + + +def purchase_order_workflow(ctx: task.OrchestrationContext, order: Order): + """Orchestrator that implements human approval pattern""" + # Auto-approve small orders + if order.cost < 1000: + return "Auto-approved" + + # Request approval for larger orders + yield ctx.call_activity(send_approval_request, input=order) + + # Wait for approval OR timeout (whichever comes first) + approval_event = ctx.wait_for_external_event("approval_received") + timeout_event = ctx.create_timer(timedelta(hours=24)) + + winner = yield task.when_any([approval_event, timeout_event]) + + if winner == timeout_event: + return "Canceled - approval timeout" + + # Order was approved + yield ctx.call_activity(place_order, input=order) + approval_details = approval_event.get_result() + return f"Approved by '{approval_details.approver}'" + + +# Raising the approval event from client +Approval = namedtuple("Approval", ["approver"]) +approval_data = Approval("manager@company.com") +client.raise_orchestration_event(instance_id, "approval_received", data=approval_data) +``` + +## Durable Timers + +Schedule delayed execution: + +```python +from datetime import timedelta + + +def delayed_workflow(ctx: task.OrchestrationContext, _): + """Orchestrator that waits before continuing""" + yield ctx.call_activity(start_activity) + + # Wait for 5 minutes (survives restarts) + yield ctx.create_timer(timedelta(minutes=5)) + + yield ctx.call_activity(continue_activity) + return "Done" +``` + +### Scheduled Execution + +Execute at a specific time: + +```python +from datetime import datetime, timedelta + + +def scheduled_workflow(ctx: task.OrchestrationContext, scheduled_time: datetime): + """Execute activity at a specific scheduled time""" + # Calculate delay from current orchestration time + delay = scheduled_time - ctx.current_utc_datetime + + if delay > timedelta(0): + yield ctx.create_timer(delay) + + result = yield ctx.call_activity(scheduled_activity) + return result +``` + +## Sub-Orchestrations + +Compose orchestrations from smaller pieces: + +```python +def child_orchestration(ctx: task.OrchestrationContext, data: str): + """Child orchestration that can be called from parents""" + result1 = yield ctx.call_activity(activity_a, input=data) + result2 = yield ctx.call_activity(activity_b, input=result1) + return result2 + + +def parent_orchestration(ctx: task.OrchestrationContext, items: list[str]): + """Parent orchestration that calls child orchestrations""" + # Call child orchestrations in parallel + tasks = [ + ctx.call_sub_orchestrator(child_orchestration, input=item) + for item in items + ] + results = yield task.when_all(tasks) + return results + + +# Registration - both must be registered +worker.add_orchestrator(parent_orchestration) +worker.add_orchestrator(child_orchestration) +worker.add_activity(activity_a) +worker.add_activity(activity_b) +``` + +## Durable Entities + +Stateful objects with operations: + +### Function-Based Entity + +```python +from durabletask import entities + + +def counter(ctx: entities.EntityContext, input: int): + """Function-based entity for a counter""" + state = ctx.get_state(int, 0) # Get state with default 0 + + if ctx.operation == "add": + state += input + ctx.set_state(state) + elif ctx.operation == "subtract": + state -= input + ctx.set_state(state) + elif ctx.operation == "get": + return state + elif ctx.operation == "reset": + ctx.set_state(0) +``` + +### Class-Based Entity + +```python +from durabletask import entities + + +class Counter(entities.DurableEntity): + """Class-based entity for a counter""" + + def __init__(self): + self.set_state(0) + + def add(self, amount: int): + current = self.get_state(int, 0) + self.set_state(current + amount) + + def subtract(self, amount: int): + current = self.get_state(int, 0) + self.set_state(current - amount) + + def get(self) -> int: + return self.get_state(int, 0) + + def reset(self): + self.set_state(0) +``` + +### Using Entities from Orchestrations + +```python +def workflow_with_entity(ctx: task.OrchestrationContext, _): + """Orchestration that interacts with entities""" + entity_id = entities.EntityInstanceId("counter", "my-counter") + + # Signal entity (fire-and-forget) + ctx.signal_entity(entity_id, operation_name="add", input=5) + + # Call entity and wait for result + value = yield ctx.call_entity(entity_id, operation_name="get") + + return f"Counter value: {value}" +``` + +### Using Entities from Client + +```python +entity_id = entities.EntityInstanceId("counter", "my-counter") +client.signal_entity(entity_id, "add", input=10) +``` + +### Entity Locking + +Ensure exclusive access to multiple entities: + +```python +def workflow_with_locks(ctx: task.OrchestrationContext, _): + """Lock multiple entities for atomic operations""" + entity_id_1 = entities.EntityInstanceId("account", "account-1") + entity_id_2 = entities.EntityInstanceId("account", "account-2") + + # Lock entities for exclusive access + with (yield ctx.lock_entities([entity_id_1, entity_id_2])): + # Perform atomic operations on both entities + balance1 = yield ctx.call_entity(entity_id_1, "get_balance") + balance2 = yield ctx.call_entity(entity_id_2, "get_balance") + + # Transfer between accounts + yield ctx.call_entity(entity_id_1, "withdraw", input=100) + yield ctx.call_entity(entity_id_2, "deposit", input=100) + + return "Transfer complete" +``` + +## Eternal Orchestrations (Continue-As-New) + +Long-running processes that periodically restart: + +```python +from datetime import timedelta + + +def eternal_orchestration(ctx: task.OrchestrationContext, iteration: int): + """Orchestration that runs forever, restarting periodically""" + # Do periodic work + yield ctx.call_activity(periodic_work, input=iteration) + + # Wait before next iteration + yield ctx.create_timer(timedelta(minutes=5)) + + # Restart with new iteration count + # This prevents history from growing unbounded + ctx.continue_as_new(iteration + 1) + + +# Start with iteration 0 +client.schedule_new_orchestration(eternal_orchestration, input=0) +``` + +## Monitoring Pattern + +Periodic polling with flexible exit conditions: + +```python +from datetime import timedelta + + +def monitoring_workflow(ctx: task.OrchestrationContext, job_id: str): + """Monitor a job until completion or timeout""" + max_attempts = 10 + polling_interval = timedelta(seconds=30) + + for attempt in range(max_attempts): + status = yield ctx.call_activity(check_job_status, input=job_id) + + if status == "completed": + return {"status": "success", "attempts": attempt + 1} + + if status == "failed": + return {"status": "failed", "attempts": attempt + 1} + + # Wait before next poll + yield ctx.create_timer(polling_interval) + + return {"status": "timeout", "attempts": max_attempts} +``` + +## Version-Aware Orchestration + +Handle breaking changes gracefully: + +```python +def versioned_orchestration(ctx: task.OrchestrationContext, order): + """Orchestrator that handles multiple versions""" + + # Check orchestration version + if ctx.version == '1.0.0': + # Old logic path + yield ctx.call_activity(activity_one) + yield ctx.call_activity(activity_two) + return "Success (v1)" + + # New logic path (v2.0.0+) + yield ctx.call_activity(activity_one) + yield ctx.call_activity(activity_three) # New activity + yield ctx.call_activity(activity_two) + return "Success (v2)" +``` + +### Worker Versioning Configuration + +```python +from durabletask.worker import VersioningOptions, VersionMatchStrategy, VersionFailureStrategy + +versioning = VersioningOptions( + default_version="2.0.0", + version="2.0.0", + match_strategy=VersionMatchStrategy.CURRENT_OR_OLDER, + failure_strategy=VersionFailureStrategy.FAIL +) + +with DurableTaskSchedulerWorker( + host_address=endpoint, + secure_channel=secure_channel, + taskhub=taskhub, + token_credential=credential, + versioning_options=versioning +) as worker: + worker.add_orchestrator(versioned_orchestration) + worker.start() +``` diff --git a/.claude/skills/durable-task-python/references/setup.md b/.claude/skills/durable-task-python/references/setup.md new file mode 100644 index 0000000..1fb16c6 --- /dev/null +++ b/.claude/skills/durable-task-python/references/setup.md @@ -0,0 +1,473 @@ +# Durable Task Scheduler Setup and Deployment + +## Local Development with Emulator + +### Docker Setup + +```bash +# Pull the emulator image +docker pull mcr.microsoft.com/dts/dts-emulator:latest + +# Run the emulator +docker run -d \ + -p 8080:8080 \ + -p 8082:8082 \ + --name dts-emulator \ + mcr.microsoft.com/dts/dts-emulator:latest + +# Emulator endpoints: +# - gRPC: http://localhost:8080 +# - Dashboard: http://localhost:8082 +``` + +### Docker Compose + +```yaml +version: '3.8' +services: + dts-emulator: + image: mcr.microsoft.com/dts/dts-emulator:latest + ports: + - "8080:8080" # gRPC endpoint + - "8082:8082" # Dashboard + restart: unless-stopped +``` + +### Default Emulator Configuration + +```python +# No authentication needed for local emulator +taskhub = "default" +endpoint = "http://localhost:8080" +credential = None +secure_channel = False +``` + +## Azure Durable Task Scheduler Provisioning + +### Prerequisites + +```bash +# Install Azure CLI +# https://learn.microsoft.com/cli/azure/install-azure-cli + +# Login to Azure +az login + +# Install durabletask extension +az extension add --name durabletask +``` + +### Create Scheduler and Task Hub + +```bash +# Set variables +RESOURCE_GROUP="my-rg" +SCHEDULER_NAME="my-scheduler" +TASKHUB_NAME="my-taskhub" +LOCATION="eastus" + +# Create resource group +az group create --name $RESOURCE_GROUP --location $LOCATION + +# Create scheduler +az durabletask scheduler create \ + --resource-group $RESOURCE_GROUP \ + --name $SCHEDULER_NAME \ + --location $LOCATION \ + --ip-allowlist "[0.0.0.0/0]" \ + --sku-capacity 1 \ + --sku-name "Dedicated" \ + --tags "environment=dev" + +# Create task hub +az durabletask taskhub create \ + --resource-group $RESOURCE_GROUP \ + --scheduler-name $SCHEDULER_NAME \ + --name $TASKHUB_NAME + +# Get endpoint URL +az durabletask scheduler show \ + --resource-group $RESOURCE_GROUP \ + --name $SCHEDULER_NAME \ + --query "properties.endpoint" -o tsv +``` + +### Assign Permissions + +```bash +# Get your user principal ID +USER_PRINCIPAL_ID=$(az ad signed-in-user show --query id -o tsv) + +# Assign Durable Task Contributor role +az role assignment create \ + --assignee $USER_PRINCIPAL_ID \ + --role "Durable Task Contributor" \ + --scope "/subscriptions/{subscription-id}/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.DurableTask/schedulers/$SCHEDULER_NAME" +``` + +## Application Configuration + +### Environment Variables + +```bash +# Bash +export ENDPOINT="https://my-scheduler.region.durabletask.io" +export TASKHUB="my-taskhub" + +# PowerShell +$env:ENDPOINT = "https://my-scheduler.region.durabletask.io" +$env:TASKHUB = "my-taskhub" +``` + +### Configuration Helper + +```python +import os +from azure.identity import DefaultAzureCredential + + +def get_connection_config(): + """Get configuration for DTS connection""" + endpoint = os.getenv("ENDPOINT", "http://localhost:8080") + taskhub = os.getenv("TASKHUB", "default") + + is_local = endpoint == "http://localhost:8080" + + return { + "host_address": endpoint, + "taskhub": taskhub, + "secure_channel": not is_local, + "token_credential": None if is_local else DefaultAzureCredential() + } +``` + +### Settings File Pattern + +```python +# settings.py +import os +from dataclasses import dataclass +from azure.identity import DefaultAzureCredential + + +@dataclass +class DurableTaskSettings: + endpoint: str + taskhub: str + secure_channel: bool + credential: any + + @classmethod + def from_environment(cls): + endpoint = os.getenv("ENDPOINT", "http://localhost:8080") + taskhub = os.getenv("TASKHUB", "default") + is_local = endpoint == "http://localhost:8080" + + return cls( + endpoint=endpoint, + taskhub=taskhub, + secure_channel=not is_local, + credential=None if is_local else DefaultAzureCredential() + ) + + +# Usage +settings = DurableTaskSettings.from_environment() +``` + +## Authentication Options + +### Local Development (No Auth) + +```python +credential = None +secure_channel = False +``` + +### DefaultAzureCredential (Recommended for Azure) + +```python +from azure.identity import DefaultAzureCredential + +credential = DefaultAzureCredential() +secure_channel = True +``` + +### Managed Identity + +```python +from azure.identity import ManagedIdentityCredential + +# System-assigned managed identity +credential = ManagedIdentityCredential() + +# User-assigned managed identity +credential = ManagedIdentityCredential(client_id="") +``` + +### Azure CLI Credential (Development) + +```python +from azure.identity import AzureCliCredential + +credential = AzureCliCredential() +``` + +## Worker Application Templates + +### Console Worker + +```python +#!/usr/bin/env python3 +"""Durable Task Worker Application""" + +import os +import signal +import sys +from azure.identity import DefaultAzureCredential +from durabletask import task +from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker + + +# Activities +def my_activity(ctx: task.ActivityContext, input: str) -> str: + return f"Processed: {input}" + + +# Orchestrations +def my_orchestration(ctx: task.OrchestrationContext, input: str): + result = yield ctx.call_activity(my_activity, input=input) + return result + + +def main(): + # Configuration + endpoint = os.getenv("ENDPOINT", "http://localhost:8080") + taskhub = os.getenv("TASKHUB", "default") + is_local = endpoint == "http://localhost:8080" + + credential = None if is_local else DefaultAzureCredential() + secure_channel = not is_local + + print(f"Starting worker...") + print(f" Endpoint: {endpoint}") + print(f" Task Hub: {taskhub}") + + with DurableTaskSchedulerWorker( + host_address=endpoint, + secure_channel=secure_channel, + taskhub=taskhub, + token_credential=credential + ) as worker: + # Register orchestrations and activities + worker.add_orchestrator(my_orchestration) + worker.add_activity(my_activity) + + # Handle shutdown signals + def shutdown(signum, frame): + print("\nShutting down worker...") + sys.exit(0) + + signal.signal(signal.SIGINT, shutdown) + signal.signal(signal.SIGTERM, shutdown) + + # Start processing + worker.start() + + print("Worker started. Press Ctrl+C to stop.") + + # Keep running + while True: + signal.pause() + + +if __name__ == "__main__": + main() +``` + +### Flask/FastAPI Integration + +```python +from flask import Flask, jsonify, request +from azure.identity import DefaultAzureCredential +from durabletask.azuremanaged.client import DurableTaskSchedulerClient +import os + +app = Flask(__name__) + +# Initialize client +endpoint = os.getenv("ENDPOINT", "http://localhost:8080") +taskhub = os.getenv("TASKHUB", "default") +is_local = endpoint == "http://localhost:8080" + +client = DurableTaskSchedulerClient( + host_address=endpoint, + secure_channel=not is_local, + taskhub=taskhub, + token_credential=None if is_local else DefaultAzureCredential() +) + + +@app.route('/orchestrations', methods=['POST']) +def start_orchestration(): + """Start a new orchestration""" + data = request.json + instance_id = client.schedule_new_orchestration( + "my_orchestration", + input=data.get("input") + ) + return jsonify({"instanceId": instance_id}), 202 + + +@app.route('/orchestrations/', methods=['GET']) +def get_status(instance_id): + """Get orchestration status""" + state = client.get_orchestration_state(instance_id) + if not state: + return jsonify({"error": "Not found"}), 404 + + return jsonify({ + "instanceId": instance_id, + "status": state.runtime_status.name, + "output": state.serialized_output + }) + + +@app.route('/orchestrations//events/', methods=['POST']) +def raise_event(instance_id, event_name): + """Raise an event on an orchestration""" + data = request.json + client.raise_orchestration_event(instance_id, event_name, data=data) + return jsonify({"status": "Event raised"}), 202 +``` + +## Deployment Options + +### Azure Container Apps + +```yaml +# container-app.yaml +properties: + configuration: + secrets: + - name: dts-endpoint + value: "https://my-scheduler.region.durabletask.io" + ingress: + external: false + template: + containers: + - image: myregistry.azurecr.io/durable-worker:latest + name: worker + env: + - name: ENDPOINT + secretRef: dts-endpoint + - name: TASKHUB + value: my-taskhub + resources: + cpu: 0.5 + memory: 1Gi + scale: + minReplicas: 1 + maxReplicas: 10 +``` + +### Kubernetes Deployment + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: durable-worker +spec: + replicas: 3 + selector: + matchLabels: + app: durable-worker + template: + metadata: + labels: + app: durable-worker + spec: + containers: + - name: worker + image: myregistry.azurecr.io/durable-worker:latest + env: + - name: ENDPOINT + valueFrom: + secretKeyRef: + name: dts-config + key: endpoint + - name: TASKHUB + valueFrom: + configMapKeyRef: + name: dts-config + key: taskhub + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi +``` + +### Docker Image + +```dockerfile +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["python", "worker.py"] +``` + +## Logging Configuration + +```python +import logging + +# Configure logging +log_handler = logging.FileHandler('durable.log', encoding='utf-8') +log_handler.setLevel(logging.DEBUG) +log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +log_handler.setFormatter(log_formatter) + +# Apply to worker +with DurableTaskSchedulerWorker( + host_address=endpoint, + secure_channel=secure_channel, + taskhub=taskhub, + token_credential=credential, + log_handler=log_handler, + log_formatter=log_formatter +) as worker: + # ... +``` + +## Monitoring + +### Dashboard Access + +- **Emulator**: http://localhost:8082 +- **Azure**: Navigate to Scheduler → Task Hub → Dashboard URL in portal + +### Query Orchestration Status + +```python +# Get all running orchestrations +# (Note: SDK provides basic queries; use dashboard for advanced filtering) + +# Check specific instance +state = client.get_orchestration_state(instance_id) +print(f"Status: {state.runtime_status}") +print(f"Created: {state.created_time}") +print(f"Updated: {state.last_updated_time}") +print(f"Input: {state.serialized_input}") +print(f"Output: {state.serialized_output}") +``` From 2c83b7de6678ba105c0bd5ddcaf815b130212cfb Mon Sep 17 00:00:00 2001 From: Tomer Rosenthal Date: Wed, 28 Jan 2026 16:09:52 -0800 Subject: [PATCH 2/7] Update SKILL.md to include additional NuGet package references for Azure Functions --- .claude/skills/durable-functions-dotnet/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/durable-functions-dotnet/SKILL.md b/.claude/skills/durable-functions-dotnet/SKILL.md index 587334d..0f00b6a 100644 --- a/.claude/skills/durable-functions-dotnet/SKILL.md +++ b/.claude/skills/durable-functions-dotnet/SKILL.md @@ -16,10 +16,10 @@ Build fault-tolerant, stateful serverless workflows using Azure Durable Function + + - - ``` From 3efc27f0a6dbf543576a24c0e55cc2a4dd0f77ea Mon Sep 17 00:00:00 2001 From: Tomer Rosenthal Date: Wed, 28 Jan 2026 16:11:56 -0800 Subject: [PATCH 3/7] Add Grpc.Net.Client package reference to SKILL.md --- .claude/skills/durable-task-dotnet/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.claude/skills/durable-task-dotnet/SKILL.md b/.claude/skills/durable-task-dotnet/SKILL.md index de1598d..46cce2b 100644 --- a/.claude/skills/durable-task-dotnet/SKILL.md +++ b/.claude/skills/durable-task-dotnet/SKILL.md @@ -17,6 +17,7 @@ Build fault-tolerant, stateful workflows in .NET applications using the Durable + ``` From 98e91683282749f0118e39299f55ac2649faa5ea Mon Sep 17 00:00:00 2001 From: Tomer Rosenthal Date: Fri, 30 Jan 2026 11:14:53 -0800 Subject: [PATCH 4/7] rename folder --- .../.claude}/skills/durable-functions-dotnet/SKILL.md | 0 .../skills/durable-functions-dotnet/references/patterns.md | 0 .../.claude}/skills/durable-functions-dotnet/references/setup.md | 0 {.claude => .github/.claude}/skills/durable-task-dotnet/SKILL.md | 0 .../.claude}/skills/durable-task-dotnet/references/patterns.md | 0 .../.claude}/skills/durable-task-dotnet/references/setup.md | 0 {.claude => .github/.claude}/skills/durable-task-java/SKILL.md | 0 .../.claude}/skills/durable-task-java/references/patterns.md | 0 .../.claude}/skills/durable-task-java/references/setup.md | 0 {.claude => .github/.claude}/skills/durable-task-python/SKILL.md | 0 .../.claude}/skills/durable-task-python/references/patterns.md | 0 .../.claude}/skills/durable-task-python/references/setup.md | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename {.claude => .github/.claude}/skills/durable-functions-dotnet/SKILL.md (100%) rename {.claude => .github/.claude}/skills/durable-functions-dotnet/references/patterns.md (100%) rename {.claude => .github/.claude}/skills/durable-functions-dotnet/references/setup.md (100%) rename {.claude => .github/.claude}/skills/durable-task-dotnet/SKILL.md (100%) rename {.claude => .github/.claude}/skills/durable-task-dotnet/references/patterns.md (100%) rename {.claude => .github/.claude}/skills/durable-task-dotnet/references/setup.md (100%) rename {.claude => .github/.claude}/skills/durable-task-java/SKILL.md (100%) rename {.claude => .github/.claude}/skills/durable-task-java/references/patterns.md (100%) rename {.claude => .github/.claude}/skills/durable-task-java/references/setup.md (100%) rename {.claude => .github/.claude}/skills/durable-task-python/SKILL.md (100%) rename {.claude => .github/.claude}/skills/durable-task-python/references/patterns.md (100%) rename {.claude => .github/.claude}/skills/durable-task-python/references/setup.md (100%) diff --git a/.claude/skills/durable-functions-dotnet/SKILL.md b/.github/.claude/skills/durable-functions-dotnet/SKILL.md similarity index 100% rename from .claude/skills/durable-functions-dotnet/SKILL.md rename to .github/.claude/skills/durable-functions-dotnet/SKILL.md diff --git a/.claude/skills/durable-functions-dotnet/references/patterns.md b/.github/.claude/skills/durable-functions-dotnet/references/patterns.md similarity index 100% rename from .claude/skills/durable-functions-dotnet/references/patterns.md rename to .github/.claude/skills/durable-functions-dotnet/references/patterns.md diff --git a/.claude/skills/durable-functions-dotnet/references/setup.md b/.github/.claude/skills/durable-functions-dotnet/references/setup.md similarity index 100% rename from .claude/skills/durable-functions-dotnet/references/setup.md rename to .github/.claude/skills/durable-functions-dotnet/references/setup.md diff --git a/.claude/skills/durable-task-dotnet/SKILL.md b/.github/.claude/skills/durable-task-dotnet/SKILL.md similarity index 100% rename from .claude/skills/durable-task-dotnet/SKILL.md rename to .github/.claude/skills/durable-task-dotnet/SKILL.md diff --git a/.claude/skills/durable-task-dotnet/references/patterns.md b/.github/.claude/skills/durable-task-dotnet/references/patterns.md similarity index 100% rename from .claude/skills/durable-task-dotnet/references/patterns.md rename to .github/.claude/skills/durable-task-dotnet/references/patterns.md diff --git a/.claude/skills/durable-task-dotnet/references/setup.md b/.github/.claude/skills/durable-task-dotnet/references/setup.md similarity index 100% rename from .claude/skills/durable-task-dotnet/references/setup.md rename to .github/.claude/skills/durable-task-dotnet/references/setup.md diff --git a/.claude/skills/durable-task-java/SKILL.md b/.github/.claude/skills/durable-task-java/SKILL.md similarity index 100% rename from .claude/skills/durable-task-java/SKILL.md rename to .github/.claude/skills/durable-task-java/SKILL.md diff --git a/.claude/skills/durable-task-java/references/patterns.md b/.github/.claude/skills/durable-task-java/references/patterns.md similarity index 100% rename from .claude/skills/durable-task-java/references/patterns.md rename to .github/.claude/skills/durable-task-java/references/patterns.md diff --git a/.claude/skills/durable-task-java/references/setup.md b/.github/.claude/skills/durable-task-java/references/setup.md similarity index 100% rename from .claude/skills/durable-task-java/references/setup.md rename to .github/.claude/skills/durable-task-java/references/setup.md diff --git a/.claude/skills/durable-task-python/SKILL.md b/.github/.claude/skills/durable-task-python/SKILL.md similarity index 100% rename from .claude/skills/durable-task-python/SKILL.md rename to .github/.claude/skills/durable-task-python/SKILL.md diff --git a/.claude/skills/durable-task-python/references/patterns.md b/.github/.claude/skills/durable-task-python/references/patterns.md similarity index 100% rename from .claude/skills/durable-task-python/references/patterns.md rename to .github/.claude/skills/durable-task-python/references/patterns.md diff --git a/.claude/skills/durable-task-python/references/setup.md b/.github/.claude/skills/durable-task-python/references/setup.md similarity index 100% rename from .claude/skills/durable-task-python/references/setup.md rename to .github/.claude/skills/durable-task-python/references/setup.md From 729074ac9c98df17f82a57c680d33fecde85e0fd Mon Sep 17 00:00:00 2001 From: Tomer Rosenthal Date: Fri, 30 Jan 2026 11:15:53 -0800 Subject: [PATCH 5/7] Add Durable Task Python SDK documentation and setup instructions - Created SKILL.md for Durable Task Python SDK overview, quick start, and usage patterns. - Added patterns.md detailing implementation patterns for function chaining, fan-out/fan-in, human interaction, durable timers, and more. - Introduced setup.md for local development with the emulator, Azure provisioning, application configuration, and deployment options. --- .github/{.claude => }/skills/durable-functions-dotnet/SKILL.md | 0 .../skills/durable-functions-dotnet/references/patterns.md | 0 .../skills/durable-functions-dotnet/references/setup.md | 0 .github/{.claude => }/skills/durable-task-dotnet/SKILL.md | 0 .../skills/durable-task-dotnet/references/patterns.md | 0 .../{.claude => }/skills/durable-task-dotnet/references/setup.md | 0 .github/{.claude => }/skills/durable-task-java/SKILL.md | 0 .../{.claude => }/skills/durable-task-java/references/patterns.md | 0 .../{.claude => }/skills/durable-task-java/references/setup.md | 0 .github/{.claude => }/skills/durable-task-python/SKILL.md | 0 .../skills/durable-task-python/references/patterns.md | 0 .../{.claude => }/skills/durable-task-python/references/setup.md | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename .github/{.claude => }/skills/durable-functions-dotnet/SKILL.md (100%) rename .github/{.claude => }/skills/durable-functions-dotnet/references/patterns.md (100%) rename .github/{.claude => }/skills/durable-functions-dotnet/references/setup.md (100%) rename .github/{.claude => }/skills/durable-task-dotnet/SKILL.md (100%) rename .github/{.claude => }/skills/durable-task-dotnet/references/patterns.md (100%) rename .github/{.claude => }/skills/durable-task-dotnet/references/setup.md (100%) rename .github/{.claude => }/skills/durable-task-java/SKILL.md (100%) rename .github/{.claude => }/skills/durable-task-java/references/patterns.md (100%) rename .github/{.claude => }/skills/durable-task-java/references/setup.md (100%) rename .github/{.claude => }/skills/durable-task-python/SKILL.md (100%) rename .github/{.claude => }/skills/durable-task-python/references/patterns.md (100%) rename .github/{.claude => }/skills/durable-task-python/references/setup.md (100%) diff --git a/.github/.claude/skills/durable-functions-dotnet/SKILL.md b/.github/skills/durable-functions-dotnet/SKILL.md similarity index 100% rename from .github/.claude/skills/durable-functions-dotnet/SKILL.md rename to .github/skills/durable-functions-dotnet/SKILL.md diff --git a/.github/.claude/skills/durable-functions-dotnet/references/patterns.md b/.github/skills/durable-functions-dotnet/references/patterns.md similarity index 100% rename from .github/.claude/skills/durable-functions-dotnet/references/patterns.md rename to .github/skills/durable-functions-dotnet/references/patterns.md diff --git a/.github/.claude/skills/durable-functions-dotnet/references/setup.md b/.github/skills/durable-functions-dotnet/references/setup.md similarity index 100% rename from .github/.claude/skills/durable-functions-dotnet/references/setup.md rename to .github/skills/durable-functions-dotnet/references/setup.md diff --git a/.github/.claude/skills/durable-task-dotnet/SKILL.md b/.github/skills/durable-task-dotnet/SKILL.md similarity index 100% rename from .github/.claude/skills/durable-task-dotnet/SKILL.md rename to .github/skills/durable-task-dotnet/SKILL.md diff --git a/.github/.claude/skills/durable-task-dotnet/references/patterns.md b/.github/skills/durable-task-dotnet/references/patterns.md similarity index 100% rename from .github/.claude/skills/durable-task-dotnet/references/patterns.md rename to .github/skills/durable-task-dotnet/references/patterns.md diff --git a/.github/.claude/skills/durable-task-dotnet/references/setup.md b/.github/skills/durable-task-dotnet/references/setup.md similarity index 100% rename from .github/.claude/skills/durable-task-dotnet/references/setup.md rename to .github/skills/durable-task-dotnet/references/setup.md diff --git a/.github/.claude/skills/durable-task-java/SKILL.md b/.github/skills/durable-task-java/SKILL.md similarity index 100% rename from .github/.claude/skills/durable-task-java/SKILL.md rename to .github/skills/durable-task-java/SKILL.md diff --git a/.github/.claude/skills/durable-task-java/references/patterns.md b/.github/skills/durable-task-java/references/patterns.md similarity index 100% rename from .github/.claude/skills/durable-task-java/references/patterns.md rename to .github/skills/durable-task-java/references/patterns.md diff --git a/.github/.claude/skills/durable-task-java/references/setup.md b/.github/skills/durable-task-java/references/setup.md similarity index 100% rename from .github/.claude/skills/durable-task-java/references/setup.md rename to .github/skills/durable-task-java/references/setup.md diff --git a/.github/.claude/skills/durable-task-python/SKILL.md b/.github/skills/durable-task-python/SKILL.md similarity index 100% rename from .github/.claude/skills/durable-task-python/SKILL.md rename to .github/skills/durable-task-python/SKILL.md diff --git a/.github/.claude/skills/durable-task-python/references/patterns.md b/.github/skills/durable-task-python/references/patterns.md similarity index 100% rename from .github/.claude/skills/durable-task-python/references/patterns.md rename to .github/skills/durable-task-python/references/patterns.md diff --git a/.github/.claude/skills/durable-task-python/references/setup.md b/.github/skills/durable-task-python/references/setup.md similarity index 100% rename from .github/.claude/skills/durable-task-python/references/setup.md rename to .github/skills/durable-task-python/references/setup.md From 97255fda335d63648177277097a14bc882200f23 Mon Sep 17 00:00:00 2001 From: Tomer Rosenthal Date: Fri, 30 Jan 2026 12:18:14 -0800 Subject: [PATCH 6/7] feat: Add Bicep modules for Azure resources including Application Insights, Log Analytics, CDN, VNet, and Key Vault - Created `applicationinsights.bicep` to provision Application Insights linked to a Log Analytics workspace. - Added `loganalytics.bicep` for creating a Log Analytics workspace. - Implemented `monitoring.bicep` to orchestrate the deployment of both Application Insights and Log Analytics. - Developed `cdn-profile.bicep` and `cdn-endpoint.bicep` for Azure CDN profile and endpoint creation. - Introduced `vnet.bicep` for creating a Virtual Network with subnets. - Created `search-services.bicep` for provisioning Azure AI Search instances. - Added security modules for role assignments in AKS, Key Vault, and Azure Container Registry. - Implemented `storage-account.bicep` for creating Azure Storage Accounts with various configurations. - Added `loadtesting.bicep` for provisioning Azure Load Testing resources. - Created `main.bicep` to serve as the entry point for deploying the infrastructure. - Added `main.parameters.json` for parameterizing the deployment. --- .../references/setup.md | 6 +- .../durable-task-java/references/setup.md | 4 +- .github/skills/durable-task-python/SKILL.md | 11 +- .../references/patterns.md | 118 +- .../durable-task-python/references/setup.md | 9 +- .gitignore | 2 +- .../infra/agent/standard-ai-hub.bicep | 2 +- .../infra/agent/standard-ai-project.bicep | 2 +- .../pdf-summarizer/infra/app/dts-Access.bicep | 2 +- .../python/pdf-summarizer/infra/app/dts.bicep | 4 +- .../dotnet/FunctionChaining/README.md | 2 + .../dotnet/FunctionChaining/azure.yaml | 2 + .../FunctionChaining/infra/app/app.bicep | 54 - .../java/function-chaining/README.md | 2 + .../java/function-chaining/azure.yaml | 2 + .../infra/abbreviations.json | 139 -- .../function-chaining/infra/app/dts.bicep | 29 - .../infra/app/user-assigned-identity.bicep | 17 - .../infra/core/ai/cognitiveservices.bicep | 56 - .../infra/core/ai/hub-dependencies.bicep | 170 --- .../function-chaining/infra/core/ai/hub.bicep | 113 -- .../infra/core/ai/project.bicep | 75 - .../infra/core/config/configstore.bicep | 48 - .../core/database/cosmos/cosmos-account.bicep | 50 - .../cosmos/mongo/cosmos-mongo-account.bicep | 23 - .../cosmos/mongo/cosmos-mongo-db.bicep | 47 - .../cosmos/sql/cosmos-sql-account.bicep | 22 - .../database/cosmos/sql/cosmos-sql-db.bicep | 74 - .../cosmos/sql/cosmos-sql-role-assign.bicep | 19 - .../cosmos/sql/cosmos-sql-role-def.bicep | 30 - .../core/database/mysql/flexibleserver.bicep | 65 - .../database/postgresql/flexibleserver.bicep | 65 - .../core/database/sqlserver/sqlserver.bicep | 130 -- .../infra/core/gateway/apim.bicep | 79 -- .../infra/core/host/ai-environment.bicep | 110 -- .../infra/core/host/aks-agent-pool.bicep | 18 - .../infra/core/host/aks-managed-cluster.bicep | 140 -- .../infra/core/host/aks.bicep | 285 ---- .../core/host/appservice-appsettings.bicep | 17 - .../infra/core/host/appservice.bicep | 74 - .../infra/core/host/appserviceplan.bicep | 19 - .../core/host/container-app-upsert.bicep | 109 -- .../infra/core/host/container-app.bicep | 169 --- .../host/container-apps-environment.bicep | 54 - .../infra/core/host/container-apps.bicep | 52 - .../infra/core/host/container-apps/app.bicep | 123 -- .../core/host/container-apps/managed.bicep | 14 - .../infra/core/host/container-registry.bicep | 137 -- .../infra/core/host/functions.bicep | 100 -- .../infra/core/host/staticwebapp.bicep | 22 - .../applicationinsights-dashboard.bicep | 1236 ----------------- .../core/monitor/applicationinsights.bicep | 31 - .../infra/core/monitor/loganalytics.bicep | 22 - .../infra/core/monitor/monitoring.bicep | 33 - .../infra/core/networking/cdn-endpoint.bicep | 52 - .../infra/core/networking/cdn-profile.bicep | 34 - .../infra/core/networking/cdn.bicep | 42 - .../infra/core/networking/vnet.bicep | 52 - .../infra/core/search/search-services.bicep | 68 - .../security/aks-managed-cluster-access.bicep | 27 - .../core/security/configstore-access.bicep | 21 - .../infra/core/security/keyvault-access.bicep | 22 - .../infra/core/security/keyvault-secret.bicep | 31 - .../infra/core/security/keyvault.bicep | 34 - .../infra/core/security/registry-access.bicep | 19 - .../infra/core/security/role.bicep | 21 - .../infra/core/storage/storage-account.bicep | 102 -- .../infra/core/testing/loadtesting.bicep | 15 - .../java/function-chaining/infra/main.bicep | 174 --- .../infra/main.parameters.json | 15 - .../python/entities/Dockerfile.client | 13 + .../python/entities/Dockerfile.worker | 13 + .../python/entities/README.md | 273 ++++ .../python/entities/azure.yaml | 27 + .../python/entities/client.py | 157 +++ .../python/entities/requirements.txt | 2 + .../python/entities/worker.py | 127 ++ .../function-chaining/Dockerfile.client | 2 +- .../function-chaining/Dockerfile.worker | 2 +- .../python/function-chaining/README.md | 2 + .../python/function-chaining/azure.yaml | 2 + .../infra/abbreviations.json | 139 -- .../function-chaining/infra/app/app.bicep | 54 - .../function-chaining/infra/app/dts.bicep | 29 - .../infra/app/user-assigned-identity.bicep | 17 - .../infra/core/ai/cognitiveservices.bicep | 56 - .../infra/core/ai/hub-dependencies.bicep | 170 --- .../function-chaining/infra/core/ai/hub.bicep | 113 -- .../infra/core/ai/project.bicep | 75 - .../infra/core/config/configstore.bicep | 48 - .../core/database/cosmos/cosmos-account.bicep | 50 - .../cosmos/mongo/cosmos-mongo-account.bicep | 23 - .../cosmos/mongo/cosmos-mongo-db.bicep | 47 - .../cosmos/sql/cosmos-sql-account.bicep | 22 - .../database/cosmos/sql/cosmos-sql-db.bicep | 74 - .../cosmos/sql/cosmos-sql-role-assign.bicep | 19 - .../cosmos/sql/cosmos-sql-role-def.bicep | 30 - .../core/database/mysql/flexibleserver.bicep | 65 - .../database/postgresql/flexibleserver.bicep | 65 - .../core/database/sqlserver/sqlserver.bicep | 130 -- .../infra/core/gateway/apim.bicep | 79 -- .../infra/core/host/ai-environment.bicep | 110 -- .../infra/core/host/aks-agent-pool.bicep | 18 - .../infra/core/host/aks-managed-cluster.bicep | 140 -- .../infra/core/host/aks.bicep | 285 ---- .../core/host/appservice-appsettings.bicep | 17 - .../infra/core/host/appservice.bicep | 74 - .../infra/core/host/appserviceplan.bicep | 19 - .../core/host/container-app-upsert.bicep | 109 -- .../infra/core/host/container-app.bicep | 169 --- .../host/container-apps-environment.bicep | 54 - .../infra/core/host/container-apps.bicep | 52 - .../infra/core/host/container-apps/app.bicep | 123 -- .../core/host/container-apps/managed.bicep | 14 - .../infra/core/host/container-registry.bicep | 137 -- .../infra/core/host/functions.bicep | 100 -- .../infra/core/host/staticwebapp.bicep | 22 - .../applicationinsights-dashboard.bicep | 1236 ----------------- .../core/monitor/applicationinsights.bicep | 31 - .../infra/core/monitor/loganalytics.bicep | 22 - .../infra/core/monitor/monitoring.bicep | 33 - .../infra/core/networking/cdn-endpoint.bicep | 52 - .../infra/core/networking/cdn-profile.bicep | 34 - .../infra/core/networking/cdn.bicep | 42 - .../infra/core/networking/vnet.bicep | 52 - .../infra/core/search/search-services.bicep | 68 - .../security/aks-managed-cluster-access.bicep | 27 - .../core/security/configstore-access.bicep | 21 - .../infra/core/security/keyvault-access.bicep | 22 - .../infra/core/security/keyvault-secret.bicep | 31 - .../infra/core/security/keyvault.bicep | 34 - .../infra/core/security/registry-access.bicep | 19 - .../infra/core/security/role.bicep | 21 - .../infra/core/storage/storage-account.bicep | 102 -- .../infra/core/testing/loadtesting.bicep | 15 - .../python/function-chaining/infra/main.bicep | 197 --- .../infra/main.parameters.json | 15 - .../python/versioning/Dockerfile.client | 13 + .../python/versioning/Dockerfile.worker | 13 + .../python/versioning/README.md | 312 +++++ .../python/versioning/azure.yaml | 27 + .../python/versioning/client.py | 131 ++ .../python/versioning/requirements.txt | 3 + .../python/versioning/worker.py | 159 +++ .../infra/abbreviations.json | 0 .../infra/app/app.bicep | 0 .../FunctionChaining => }/infra/app/dts.bicep | 4 +- .../infra/app/user-assigned-identity.bicep | 0 .../infra/core/ai/cognitiveservices.bicep | 0 .../infra/core/ai/hub-dependencies.bicep | 0 .../infra/core/ai/hub.bicep | 0 .../infra/core/ai/project.bicep | 0 .../infra/core/config/configstore.bicep | 0 .../core/database/cosmos/cosmos-account.bicep | 0 .../cosmos/mongo/cosmos-mongo-account.bicep | 0 .../cosmos/mongo/cosmos-mongo-db.bicep | 0 .../cosmos/sql/cosmos-sql-account.bicep | 0 .../database/cosmos/sql/cosmos-sql-db.bicep | 0 .../cosmos/sql/cosmos-sql-role-assign.bicep | 0 .../cosmos/sql/cosmos-sql-role-def.bicep | 0 .../core/database/mysql/flexibleserver.bicep | 0 .../database/postgresql/flexibleserver.bicep | 0 .../core/database/sqlserver/sqlserver.bicep | 0 .../infra/core/gateway/apim.bicep | 0 .../infra/core/host/ai-environment.bicep | 0 .../infra/core/host/aks-agent-pool.bicep | 0 .../infra/core/host/aks-managed-cluster.bicep | 0 .../infra/core/host/aks.bicep | 0 .../core/host/appservice-appsettings.bicep | 0 .../infra/core/host/appservice.bicep | 0 .../infra/core/host/appserviceplan.bicep | 0 .../core/host/container-app-upsert.bicep | 0 .../infra/core/host/container-app.bicep | 0 .../host/container-apps-environment.bicep | 0 .../infra/core/host/container-apps.bicep | 0 .../infra/core/host/container-apps/app.bicep | 0 .../core/host/container-apps/managed.bicep | 0 .../infra/core/host/container-registry.bicep | 0 .../infra/core/host/functions.bicep | 0 .../infra/core/host/staticwebapp.bicep | 0 .../applicationinsights-dashboard.bicep | 0 .../core/monitor/applicationinsights.bicep | 0 .../infra/core/monitor/loganalytics.bicep | 0 .../infra/core/monitor/monitoring.bicep | 0 .../infra/core/networking/cdn-endpoint.bicep | 0 .../infra/core/networking/cdn-profile.bicep | 0 .../infra/core/networking/cdn.bicep | 0 .../infra/core/networking/vnet.bicep | 0 .../infra/core/search/search-services.bicep | 0 .../security/aks-managed-cluster-access.bicep | 0 .../core/security/configstore-access.bicep | 0 .../infra/core/security/keyvault-access.bicep | 0 .../infra/core/security/keyvault-secret.bicep | 0 .../infra/core/security/keyvault.bicep | 0 .../infra/core/security/registry-access.bicep | 0 .../infra/core/security/role.bicep | 0 .../infra/core/storage/storage-account.bicep | 0 .../infra/core/testing/loadtesting.bicep | 0 .../FunctionChaining => }/infra/main.bicep | 0 .../infra/main.parameters.json | 0 200 files changed, 1386 insertions(+), 9685 deletions(-) delete mode 100644 samples/durable-task-sdks/dotnet/FunctionChaining/infra/app/app.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/abbreviations.json delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/app/dts.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/app/user-assigned-identity.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/ai/cognitiveservices.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/ai/hub-dependencies.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/ai/hub.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/ai/project.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/config/configstore.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/cosmos-account.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-account.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-db.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/database/mysql/flexibleserver.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/database/postgresql/flexibleserver.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/database/sqlserver/sqlserver.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/gateway/apim.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/ai-environment.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/aks-agent-pool.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/aks-managed-cluster.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/aks.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/appservice-appsettings.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/appservice.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/appserviceplan.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/container-app-upsert.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/container-app.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps-environment.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps/app.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps/managed.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/container-registry.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/functions.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/host/staticwebapp.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/monitor/applicationinsights-dashboard.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/monitor/applicationinsights.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/monitor/loganalytics.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/monitor/monitoring.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/networking/cdn-endpoint.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/networking/cdn-profile.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/networking/cdn.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/networking/vnet.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/search/search-services.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/security/aks-managed-cluster-access.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/security/configstore-access.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/security/keyvault-access.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/security/keyvault-secret.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/security/keyvault.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/security/registry-access.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/security/role.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/storage/storage-account.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/core/testing/loadtesting.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/main.bicep delete mode 100644 samples/durable-task-sdks/java/function-chaining/infra/main.parameters.json create mode 100644 samples/durable-task-sdks/python/entities/Dockerfile.client create mode 100644 samples/durable-task-sdks/python/entities/Dockerfile.worker create mode 100644 samples/durable-task-sdks/python/entities/README.md create mode 100644 samples/durable-task-sdks/python/entities/azure.yaml create mode 100644 samples/durable-task-sdks/python/entities/client.py create mode 100644 samples/durable-task-sdks/python/entities/requirements.txt create mode 100644 samples/durable-task-sdks/python/entities/worker.py delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/abbreviations.json delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/app/app.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/app/dts.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/app/user-assigned-identity.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/ai/cognitiveservices.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/ai/hub-dependencies.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/ai/hub.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/ai/project.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/config/configstore.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/cosmos-account.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-account.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-db.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/database/mysql/flexibleserver.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/database/postgresql/flexibleserver.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/database/sqlserver/sqlserver.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/gateway/apim.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/ai-environment.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/aks-agent-pool.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/aks-managed-cluster.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/aks.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/appservice-appsettings.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/appservice.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/appserviceplan.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/container-app-upsert.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/container-app.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps-environment.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps/app.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps/managed.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/container-registry.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/functions.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/host/staticwebapp.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/monitor/applicationinsights-dashboard.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/monitor/applicationinsights.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/monitor/loganalytics.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/monitor/monitoring.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/networking/cdn-endpoint.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/networking/cdn-profile.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/networking/cdn.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/networking/vnet.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/search/search-services.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/security/aks-managed-cluster-access.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/security/configstore-access.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/security/keyvault-access.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/security/keyvault-secret.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/security/keyvault.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/security/registry-access.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/security/role.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/storage/storage-account.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/core/testing/loadtesting.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/main.bicep delete mode 100644 samples/durable-task-sdks/python/function-chaining/infra/main.parameters.json create mode 100644 samples/durable-task-sdks/python/versioning/Dockerfile.client create mode 100644 samples/durable-task-sdks/python/versioning/Dockerfile.worker create mode 100644 samples/durable-task-sdks/python/versioning/README.md create mode 100644 samples/durable-task-sdks/python/versioning/azure.yaml create mode 100644 samples/durable-task-sdks/python/versioning/client.py create mode 100644 samples/durable-task-sdks/python/versioning/requirements.txt create mode 100644 samples/durable-task-sdks/python/versioning/worker.py rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/abbreviations.json (100%) rename samples/{durable-task-sdks/java/function-chaining => }/infra/app/app.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/app/dts.bicep (75%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/app/user-assigned-identity.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/ai/cognitiveservices.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/ai/hub-dependencies.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/ai/hub.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/ai/project.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/config/configstore.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/database/cosmos/cosmos-account.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/database/cosmos/sql/cosmos-sql-account.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/database/cosmos/sql/cosmos-sql-db.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/database/mysql/flexibleserver.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/database/postgresql/flexibleserver.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/database/sqlserver/sqlserver.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/gateway/apim.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/ai-environment.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/aks-agent-pool.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/aks-managed-cluster.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/aks.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/appservice-appsettings.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/appservice.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/appserviceplan.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/container-app-upsert.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/container-app.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/container-apps-environment.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/container-apps.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/container-apps/app.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/container-apps/managed.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/container-registry.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/functions.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/host/staticwebapp.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/monitor/applicationinsights-dashboard.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/monitor/applicationinsights.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/monitor/loganalytics.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/monitor/monitoring.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/networking/cdn-endpoint.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/networking/cdn-profile.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/networking/cdn.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/networking/vnet.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/search/search-services.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/security/aks-managed-cluster-access.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/security/configstore-access.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/security/keyvault-access.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/security/keyvault-secret.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/security/keyvault.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/security/registry-access.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/security/role.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/storage/storage-account.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/core/testing/loadtesting.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/main.bicep (100%) rename samples/{durable-task-sdks/dotnet/FunctionChaining => }/infra/main.parameters.json (100%) diff --git a/.github/skills/durable-functions-dotnet/references/setup.md b/.github/skills/durable-functions-dotnet/references/setup.md index d244968..03b9eae 100644 --- a/.github/skills/durable-functions-dotnet/references/setup.md +++ b/.github/skills/durable-functions-dotnet/references/setup.md @@ -442,7 +442,7 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { } // Durable Task Scheduler Namespace -resource dtsNamespace 'Microsoft.DurableTask/namespaces@2024-10-01-preview' = { +resource dtsNamespace 'Microsoft.DurableTask/namespaces@2025-11-01' = { name: '${baseName}-dts' location: location sku: { @@ -453,7 +453,7 @@ resource dtsNamespace 'Microsoft.DurableTask/namespaces@2024-10-01-preview' = { } // Scheduler -resource scheduler 'Microsoft.DurableTask/namespaces/schedulers@2024-10-01-preview' = { +resource scheduler 'Microsoft.DurableTask/namespaces/schedulers@2025-11-01' = { parent: dtsNamespace name: 'scheduler' location: location @@ -469,7 +469,7 @@ resource scheduler 'Microsoft.DurableTask/namespaces/schedulers@2024-10-01-previ } // Task Hub -resource taskHub 'Microsoft.DurableTask/namespaces/taskHubs@2024-10-01-preview' = { +resource taskHub 'Microsoft.DurableTask/namespaces/taskHubs@2025-11-01' = { parent: dtsNamespace name: 'default' properties: {} diff --git a/.github/skills/durable-task-java/references/setup.md b/.github/skills/durable-task-java/references/setup.md index d113e54..4b4707b 100644 --- a/.github/skills/durable-task-java/references/setup.md +++ b/.github/skills/durable-task-java/references/setup.md @@ -139,7 +139,7 @@ param taskHubName string = 'default' @allowed(['basic', 'standard', 'premium']) param sku string = 'standard' -resource scheduler 'Microsoft.DurableTask/schedulers@2024-10-01-preview' = { +resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' = { name: schedulerName location: location properties: { @@ -149,7 +149,7 @@ resource scheduler 'Microsoft.DurableTask/schedulers@2024-10-01-preview' = { } } -resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2024-10-01-preview' = { +resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2025-11-01' = { parent: scheduler name: taskHubName properties: {} diff --git a/.github/skills/durable-task-python/SKILL.md b/.github/skills/durable-task-python/SKILL.md index 1edce16..1defe11 100644 --- a/.github/skills/durable-task-python/SKILL.md +++ b/.github/skills/durable-task-python/SKILL.md @@ -28,7 +28,8 @@ azure-identity ```python import os from azure.identity import DefaultAzureCredential -from durabletask import client, task +from durabletask import task +from durabletask.client import OrchestrationStatus from durabletask.azuremanaged.client import DurableTaskSchedulerClient from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker @@ -62,17 +63,17 @@ with DurableTaskSchedulerWorker( worker.start() # Create client and schedule orchestration - client = DurableTaskSchedulerClient( + dts_client = DurableTaskSchedulerClient( host_address=endpoint, secure_channel=secure_channel, taskhub=taskhub, token_credential=credential ) - instance_id = client.schedule_new_orchestration(my_orchestration, input="World") - state = client.wait_for_orchestration_completion(instance_id, timeout=60) + instance_id = dts_client.schedule_new_orchestration(my_orchestration, input="World") + state = dts_client.wait_for_orchestration_completion(instance_id, timeout=60) - if state and state.runtime_status == client.OrchestrationStatus.COMPLETED: + if state and state.runtime_status == OrchestrationStatus.COMPLETED: print(f"Result: {state.serialized_output}") ``` diff --git a/.github/skills/durable-task-python/references/patterns.md b/.github/skills/durable-task-python/references/patterns.md index 87ee827..d9f9e1d 100644 --- a/.github/skills/durable-task-python/references/patterns.md +++ b/.github/skills/durable-task-python/references/patterns.md @@ -278,11 +278,11 @@ def workflow_with_entity(ctx: task.OrchestrationContext, _): """Orchestration that interacts with entities""" entity_id = entities.EntityInstanceId("counter", "my-counter") - # Signal entity (fire-and-forget) - ctx.signal_entity(entity_id, operation_name="add", input=5) + # Signal entity (fire-and-forget) - uses entity_id and operation_name params + ctx.signal_entity(entity_id=entity_id, operation_name="add", input=5) - # Call entity and wait for result - value = yield ctx.call_entity(entity_id, operation_name="get") + # Call entity and wait for result - uses entity and operation params (different from signal!) + value = yield ctx.call_entity(entity=entity_id, operation="get") return f"Counter value: {value}" ``` @@ -307,12 +307,13 @@ def workflow_with_locks(ctx: task.OrchestrationContext, _): # Lock entities for exclusive access with (yield ctx.lock_entities([entity_id_1, entity_id_2])): # Perform atomic operations on both entities - balance1 = yield ctx.call_entity(entity_id_1, "get_balance") - balance2 = yield ctx.call_entity(entity_id_2, "get_balance") + # Note: call_entity uses 'entity' and 'operation' params + balance1 = yield ctx.call_entity(entity=entity_id_1, operation="get_balance") + balance2 = yield ctx.call_entity(entity=entity_id_2, operation="get_balance") # Transfer between accounts - yield ctx.call_entity(entity_id_1, "withdraw", input=100) - yield ctx.call_entity(entity_id_2, "deposit", input=100) + yield ctx.call_entity(entity=entity_id_1, operation="withdraw", input=100) + yield ctx.call_entity(entity=entity_id_2, operation="deposit", input=100) return "Transfer complete" ``` @@ -372,45 +373,80 @@ def monitoring_workflow(ctx: task.OrchestrationContext, job_id: str): ## Version-Aware Orchestration -Handle breaking changes gracefully: +Handle breaking changes gracefully using orchestration versioning. Version is set when scheduling the orchestration and read via `ctx.version`. + +### Setting Version When Scheduling ```python -def versioned_orchestration(ctx: task.OrchestrationContext, order): - """Orchestrator that handles multiple versions""" - - # Check orchestration version - if ctx.version == '1.0.0': - # Old logic path - yield ctx.call_activity(activity_one) - yield ctx.call_activity(activity_two) - return "Success (v1)" - - # New logic path (v2.0.0+) - yield ctx.call_activity(activity_one) - yield ctx.call_activity(activity_three) # New activity - yield ctx.call_activity(activity_two) - return "Success (v2)" +# Schedule orchestration with a specific version +instance_id = client.schedule_new_orchestration( + "versioned_orchestration", + input="data", + version="2.0.0" # Version is set here +) ``` -### Worker Versioning Configuration +### Version Comparison Helper + +Use the `packaging` module for semantic version comparison: ```python -from durabletask.worker import VersioningOptions, VersionMatchStrategy, VersionFailureStrategy +from packaging import version + +def compare_version(v1: str | None, v2: str) -> int: + """Compare two version strings. + + Returns: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2 + """ + if v1 is None: + return -1 + try: + ver1 = version.parse(v1) + ver2 = version.parse(v2) + if ver1 < ver2: + return -1 + elif ver1 > ver2: + return 1 + return 0 + except Exception: + # Fall back to string comparison + return (v1 > v2) - (v1 < v2) +``` -versioning = VersioningOptions( - default_version="2.0.0", - version="2.0.0", - match_strategy=VersionMatchStrategy.CURRENT_OR_OLDER, - failure_strategy=VersionFailureStrategy.FAIL -) +### Versioned Orchestration Example -with DurableTaskSchedulerWorker( - host_address=endpoint, - secure_channel=secure_channel, - taskhub=taskhub, - token_credential=credential, - versioning_options=versioning -) as worker: - worker.add_orchestrator(versioned_orchestration) - worker.start() +```python +def versioned_orchestration(ctx: task.OrchestrationContext, name: str): + """Orchestrator that handles multiple versions. + + Version history: + - v1.0.0: Basic hello greeting + - v2.0.0: Added goodbye greeting + """ + results = [] + orch_version = ctx.version # Read version set during scheduling + + # v1.0.0+: Always run this + hello = yield ctx.call_activity(say_hello, input=name) + results.append(hello) + + # v2.0.0+: Added in version 2 + if compare_version(orch_version, "2.0.0") >= 0: + goodbye = yield ctx.call_activity(say_goodbye, input=name) + results.append(goodbye) + + return {"version": orch_version, "results": results} ``` + +### Why Versioning Matters + +Without versioning, changing orchestration logic causes **non-deterministic errors**: +1. v1 orchestration starts (calls activity A, then B) +2. You deploy v2 code (calls A, C, then B) +3. v1 orchestration replays but hits new code path +4. **ERROR**: History doesn't match - expected B, got C + +With versioning: +- v1 orchestrations continue using v1 code path +- v2 orchestrations use v2 code path +- Both run on the same worker without conflict diff --git a/.github/skills/durable-task-python/references/setup.md b/.github/skills/durable-task-python/references/setup.md index 1fb16c6..1b9b469 100644 --- a/.github/skills/durable-task-python/references/setup.md +++ b/.github/skills/durable-task-python/references/setup.md @@ -277,9 +277,12 @@ def main(): print("Worker started. Press Ctrl+C to stop.") - # Keep running - while True: - signal.pause() + # Keep running (cross-platform) + import asyncio + try: + asyncio.get_event_loop().run_forever() + except KeyboardInterrupt: + pass if __name__ == "__main__": diff --git a/.gitignore b/.gitignore index 1c3d923..d58841b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ *.userosscache *.sln.docstates **/.DS_Store - +venv/ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/samples/durable-functions/dotnet/AiAgentTravelPlanOrchestrator/infra/agent/standard-ai-hub.bicep b/samples/durable-functions/dotnet/AiAgentTravelPlanOrchestrator/infra/agent/standard-ai-hub.bicep index 220a0e2..3c83e13 100644 --- a/samples/durable-functions/dotnet/AiAgentTravelPlanOrchestrator/infra/agent/standard-ai-hub.bicep +++ b/samples/durable-functions/dotnet/AiAgentTravelPlanOrchestrator/infra/agent/standard-ai-hub.bicep @@ -97,7 +97,7 @@ resource aiHub 'Microsoft.MachineLearningServices/workspaces@2024-07-01-preview' } // Resource definition for the capability host - resource capabilityHost 'capabilityHosts@2024-10-01-preview' = { + resource capabilityHost 'capabilityHosts@2025-11-01' = { name: '${aiHubName}-${capabilityHostName}' properties: { capabilityHostKind: 'Agents' diff --git a/samples/durable-functions/dotnet/AiAgentTravelPlanOrchestrator/infra/agent/standard-ai-project.bicep b/samples/durable-functions/dotnet/AiAgentTravelPlanOrchestrator/infra/agent/standard-ai-project.bicep index 48796ed..a81b95d 100644 --- a/samples/durable-functions/dotnet/AiAgentTravelPlanOrchestrator/infra/agent/standard-ai-project.bicep +++ b/samples/durable-functions/dotnet/AiAgentTravelPlanOrchestrator/infra/agent/standard-ai-project.bicep @@ -60,7 +60,7 @@ resource aiProject 'Microsoft.MachineLearningServices/workspaces@2023-08-01-prev kind: 'project' // Resource definition for the capability host - resource capabilityHost 'capabilityHosts@2024-10-01-preview' = { + resource capabilityHost 'capabilityHosts@2025-11-01' = { name: '${aiProjectName}-${capabilityHostName}' properties: { capabilityHostKind: 'Agents' diff --git a/samples/durable-functions/python/pdf-summarizer/infra/app/dts-Access.bicep b/samples/durable-functions/python/pdf-summarizer/infra/app/dts-Access.bicep index 1e8051b..a0935bf 100644 --- a/samples/durable-functions/python/pdf-summarizer/infra/app/dts-Access.bicep +++ b/samples/durable-functions/python/pdf-summarizer/infra/app/dts-Access.bicep @@ -3,7 +3,7 @@ param roleDefinitionID string param dtsName string param principalType string -resource dts 'Microsoft.DurableTask/schedulers@2024-10-01-preview' existing = { +resource dts 'Microsoft.DurableTask/schedulers@2025-11-01' existing = { name: dtsName } diff --git a/samples/durable-functions/python/pdf-summarizer/infra/app/dts.bicep b/samples/durable-functions/python/pdf-summarizer/infra/app/dts.bicep index c435832..5a08700 100644 --- a/samples/durable-functions/python/pdf-summarizer/infra/app/dts.bicep +++ b/samples/durable-functions/python/pdf-summarizer/infra/app/dts.bicep @@ -6,7 +6,7 @@ param taskhubname string param skuName string param skuCapacity int -resource dts 'Microsoft.DurableTask/schedulers@2024-10-01-preview' = { +resource dts 'Microsoft.DurableTask/schedulers@2025-11-01' = { location: location tags: tags name: name @@ -19,7 +19,7 @@ resource dts 'Microsoft.DurableTask/schedulers@2024-10-01-preview' = { } } -resource taskhub 'Microsoft.DurableTask/schedulers/taskhubs@2024-10-01-preview' = { +resource taskhub 'Microsoft.DurableTask/schedulers/taskhubs@2025-11-01' = { parent: dts name: taskhubname } diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/README.md b/samples/durable-task-sdks/dotnet/FunctionChaining/README.md index 66903d8..53b3d1f 100644 --- a/samples/durable-task-sdks/dotnet/FunctionChaining/README.md +++ b/samples/durable-task-sdks/dotnet/FunctionChaining/README.md @@ -150,6 +150,8 @@ Once you have set up either the emulator or deployed scheduler, follow these ste This sample includes an `azure.yaml` configuration file that allows you to deploy the entire solution to Azure using Azure Developer CLI (AZD). +> **Note:** This sample uses the shared infrastructure templates located at [`samples/infra/`](../../infra/). + #### Prerequisites for AZD Deployment 1. Install [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/azure.yaml b/samples/durable-task-sdks/dotnet/FunctionChaining/azure.yaml index 843ba4a..7e023e2 100644 --- a/samples/durable-task-sdks/dotnet/FunctionChaining/azure.yaml +++ b/samples/durable-task-sdks/dotnet/FunctionChaining/azure.yaml @@ -8,6 +8,8 @@ metadata: template: hello-azd-dotnet name: dts-quiockstart +infra: + path: ../../infra services: client: project: ./Client diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/app/app.bicep b/samples/durable-task-sdks/dotnet/FunctionChaining/infra/app/app.bicep deleted file mode 100644 index bc7107b..0000000 --- a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/app/app.bicep +++ /dev/null @@ -1,54 +0,0 @@ -param appName string -param location string = resourceGroup().location -param tags object = {} - -param identityName string -param containerAppsEnvironmentName string -param containerRegistryName string -param serviceName string = 'aca' -param exists bool -param dtsEndpoint string -param taskHubName string - -type managedIdentity = { - resourceId: string - clientId: string -} - -@description('Unique identifier for user-assigned managed identity.') -param userAssignedManagedIdentity managedIdentity - -module containerAppsApp '../core/host/container-app.bicep' = { - name: 'container-apps-${serviceName}' - params: { - name: appName - containerAppsEnvironmentName: containerAppsEnvironmentName - containerRegistryName: containerRegistryName - location: location - tags: union(tags, { 'azd-service-name': serviceName }) - ingressEnabled: false - secrets: { - 'azure-managed-identity-client-id': userAssignedManagedIdentity.clientId - } - env: [ - { - name: 'AZURE_MANAGED_IDENTITY_CLIENT_ID' - secretRef: 'azure-managed-identity-client-id' - } - { - name: 'ENDPOINT' - value: 'Endpoint=${dtsEndpoint};Authentication=ManagedIdentity;ClientID=${userAssignedManagedIdentity.clientId}' - } - { - name: 'TASKHUB' - value: taskHubName - } - ] - identityName: identityName - containerMinReplicas: 0 - containerMaxReplicas: 10 - } -} - -output endpoint string = containerAppsApp.outputs.uri -output envName string = containerAppsApp.outputs.name diff --git a/samples/durable-task-sdks/java/function-chaining/README.md b/samples/durable-task-sdks/java/function-chaining/README.md index eabc1cc..084e4b3 100644 --- a/samples/durable-task-sdks/java/function-chaining/README.md +++ b/samples/durable-task-sdks/java/function-chaining/README.md @@ -108,6 +108,8 @@ cd function-chaining This sample includes an `azure.yaml` configuration file that allows you to deploy the sample to Azure using Azure Developer CLI (AZD). +> **Note:** This sample uses the shared infrastructure templates located at [`samples/infra/`](../../../infra/). + #### Prerequisites for AZD Deployment 1. Install [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) diff --git a/samples/durable-task-sdks/java/function-chaining/azure.yaml b/samples/durable-task-sdks/java/function-chaining/azure.yaml index abfd537..df46055 100644 --- a/samples/durable-task-sdks/java/function-chaining/azure.yaml +++ b/samples/durable-task-sdks/java/function-chaining/azure.yaml @@ -8,6 +8,8 @@ metadata: template: hello-azd-java name: dts-quickstart +infra: + path: ../../../infra services: sampleapp: project: . diff --git a/samples/durable-task-sdks/java/function-chaining/infra/abbreviations.json b/samples/durable-task-sdks/java/function-chaining/infra/abbreviations.json deleted file mode 100644 index 1f9a112..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/abbreviations.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "analysisServicesServers": "as", - "apiManagementService": "apim-", - "appConfigurationStores": "appcs-", - "appManagedEnvironments": "cae-", - "appContainerApps": "ca-", - "authorizationPolicyDefinitions": "policy-", - "automationAutomationAccounts": "aa-", - "blueprintBlueprints": "bp-", - "blueprintBlueprintsArtifacts": "bpa-", - "cacheRedis": "redis-", - "cdnProfiles": "cdnp-", - "cdnProfilesEndpoints": "cdne-", - "cognitiveServicesAccounts": "cog-", - "cognitiveServicesFormRecognizer": "cog-fr-", - "cognitiveServicesTextAnalytics": "cog-ta-", - "cognitiveServicesSpeech": "cog-sp-", - "computeAvailabilitySets": "avail-", - "computeCloudServices": "cld-", - "computeDiskEncryptionSets": "des", - "computeDisks": "disk", - "computeDisksOs": "osdisk", - "computeGalleries": "gal", - "computeSnapshots": "snap-", - "computeVirtualMachines": "vm", - "computeVirtualMachineScaleSets": "vmss-", - "containerInstanceContainerGroups": "ci", - "containerRegistryRegistries": "cr", - "containerServiceManagedClusters": "aks-", - "databricksWorkspaces": "dbw-", - "dataFactoryFactories": "adf-", - "dataLakeAnalyticsAccounts": "dla", - "dataLakeStoreAccounts": "dls", - "dataMigrationServices": "dms-", - "dBforMySQLServers": "mysql-", - "dBforPostgreSQLServers": "psql-", - "devicesIotHubs": "iot-", - "devicesProvisioningServices": "provs-", - "devicesProvisioningServicesCertificates": "pcert-", - "documentDBDatabaseAccounts": "cosmos-", - "eventGridDomains": "evgd-", - "eventGridDomainsTopics": "evgt-", - "eventGridEventSubscriptions": "evgs-", - "eventHubNamespaces": "evhns-", - "eventHubNamespacesEventHubs": "evh-", - "hdInsightClustersHadoop": "hadoop-", - "hdInsightClustersHbase": "hbase-", - "hdInsightClustersKafka": "kafka-", - "hdInsightClustersMl": "mls-", - "hdInsightClustersSpark": "spark-", - "hdInsightClustersStorm": "storm-", - "hybridComputeMachines": "arcs-", - "insightsActionGroups": "ag-", - "insightsComponents": "appi-", - "keyVaultVaults": "kv-", - "kubernetesConnectedClusters": "arck", - "kustoClusters": "dec", - "kustoClustersDatabases": "dedb", - "loadTesting": "lt-", - "logicIntegrationAccounts": "ia-", - "logicWorkflows": "logic-", - "machineLearningServicesWorkspaces": "mlw-", - "managedIdentityUserAssignedIdentities": "id-", - "managementManagementGroups": "mg-", - "migrateAssessmentProjects": "migr-", - "networkApplicationGateways": "agw-", - "networkApplicationSecurityGroups": "asg-", - "networkAzureFirewalls": "afw-", - "networkBastionHosts": "bas-", - "networkConnections": "con-", - "networkDnsZones": "dnsz-", - "networkExpressRouteCircuits": "erc-", - "networkFirewallPolicies": "afwp-", - "networkFirewallPoliciesWebApplication": "waf", - "networkFirewallPoliciesRuleGroups": "wafrg", - "networkFrontDoors": "fd-", - "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", - "networkLoadBalancersExternal": "lbe-", - "networkLoadBalancersInternal": "lbi-", - "networkLoadBalancersInboundNatRules": "rule-", - "networkLocalNetworkGateways": "lgw-", - "networkNatGateways": "ng-", - "networkNetworkInterfaces": "nic-", - "networkNetworkSecurityGroups": "nsg-", - "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", - "networkNetworkWatchers": "nw-", - "networkPrivateDnsZones": "pdnsz-", - "networkPrivateLinkServices": "pl-", - "networkPublicIPAddresses": "pip-", - "networkPublicIPPrefixes": "ippre-", - "networkRouteFilters": "rf-", - "networkRouteTables": "rt-", - "networkRouteTablesRoutes": "udr-", - "networkTrafficManagerProfiles": "traf-", - "networkVirtualNetworkGateways": "vgw-", - "networkVirtualNetworks": "vnet-", - "networkVirtualNetworksSubnets": "snet-", - "networkVirtualNetworksVirtualNetworkPeerings": "peer-", - "networkVirtualWans": "vwan-", - "networkVpnGateways": "vpng-", - "networkVpnGatewaysVpnConnections": "vcn-", - "networkVpnGatewaysVpnSites": "vst-", - "notificationHubsNamespaces": "ntfns-", - "notificationHubsNamespacesNotificationHubs": "ntf-", - "operationalInsightsWorkspaces": "log-", - "portalDashboards": "dash-", - "powerBIDedicatedCapacities": "pbi-", - "purviewAccounts": "pview-", - "recoveryServicesVaults": "rsv-", - "resourcesResourceGroups": "rg-", - "searchSearchServices": "srch-", - "serviceBusNamespaces": "sb-", - "serviceBusNamespacesQueues": "sbq-", - "serviceBusNamespacesTopics": "sbt-", - "serviceEndPointPolicies": "se-", - "serviceFabricClusters": "sf-", - "signalRServiceSignalR": "sigr", - "sqlManagedInstances": "sqlmi-", - "sqlServers": "sql-", - "sqlServersDataWarehouse": "sqldw-", - "sqlServersDatabases": "sqldb-", - "sqlServersDatabasesStretch": "sqlstrdb-", - "storageStorageAccounts": "st", - "storageStorageAccountsVm": "stvm", - "storSimpleManagers": "ssimp", - "streamAnalyticsCluster": "asa-", - "synapseWorkspaces": "syn", - "synapseWorkspacesAnalyticsWorkspaces": "synw", - "synapseWorkspacesSqlPoolsDedicated": "syndp", - "synapseWorkspacesSqlPoolsSpark": "synsp", - "timeSeriesInsightsEnvironments": "tsi-", - "webServerFarms": "plan-", - "webSitesAppService": "app-", - "webSitesAppServiceEnvironment": "ase-", - "webSitesFunctions": "func-", - "webStaticSites": "stapp-", - "dts": "dts-", - "taskhub": "taskhub-" -} diff --git a/samples/durable-task-sdks/java/function-chaining/infra/app/dts.bicep b/samples/durable-task-sdks/java/function-chaining/infra/app/dts.bicep deleted file mode 100644 index c435832..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/app/dts.bicep +++ /dev/null @@ -1,29 +0,0 @@ -param ipAllowlist array -param location string -param tags object = {} -param name string -param taskhubname string -param skuName string -param skuCapacity int - -resource dts 'Microsoft.DurableTask/schedulers@2024-10-01-preview' = { - location: location - tags: tags - name: name - properties: { - ipAllowlist: ipAllowlist - sku: { - name: skuName - capacity: skuCapacity - } - } -} - -resource taskhub 'Microsoft.DurableTask/schedulers/taskhubs@2024-10-01-preview' = { - parent: dts - name: taskhubname -} - -output dts_NAME string = dts.name -output dts_URL string = dts.properties.endpoint -output TASKHUB_NAME string = taskhub.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/app/user-assigned-identity.bicep b/samples/durable-task-sdks/java/function-chaining/infra/app/user-assigned-identity.bicep deleted file mode 100644 index 0583ab8..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/app/user-assigned-identity.bicep +++ /dev/null @@ -1,17 +0,0 @@ -metadata description = 'Creates a Microsoft Entra user-assigned identity.' - -param name string -param location string = resourceGroup().location -param tags object = {} - -resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { - name: name - location: location - tags: tags -} - -output name string = identity.name -output resourceId string = identity.id -output principalId string = identity.properties.principalId -output clientId string = identity.properties.clientId -output tenantId string = identity.properties.tenantId diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/ai/cognitiveservices.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/ai/cognitiveservices.bicep deleted file mode 100644 index 76778e6..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/ai/cognitiveservices.bicep +++ /dev/null @@ -1,56 +0,0 @@ -metadata description = 'Creates an Azure Cognitive Services instance.' -param name string -param location string = resourceGroup().location -param tags object = {} -@description('The custom subdomain name used to access the API. Defaults to the value of the name parameter.') -param customSubDomainName string = name -param disableLocalAuth bool = false -param deployments array = [] -param kind string = 'OpenAI' - -@allowed([ 'Enabled', 'Disabled' ]) -param publicNetworkAccess string = 'Enabled' -param sku object = { - name: 'S0' -} - -param allowedIpRules array = [] -param networkAcls object = empty(allowedIpRules) ? { - defaultAction: 'Allow' -} : { - ipRules: allowedIpRules - defaultAction: 'Deny' -} - -resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { - name: name - location: location - tags: tags - kind: kind - properties: { - customSubDomainName: customSubDomainName - publicNetworkAccess: publicNetworkAccess - networkAcls: networkAcls - disableLocalAuth: disableLocalAuth - } - sku: sku -} - -@batchSize(1) -resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in deployments: { - parent: account - name: deployment.name - properties: { - model: deployment.model - raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null - } - sku: contains(deployment, 'sku') ? deployment.sku : { - name: 'Standard' - capacity: 20 - } -}] - -output endpoint string = account.properties.endpoint -output endpoints object = account.properties.endpoints -output id string = account.id -output name string = account.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/ai/hub-dependencies.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/ai/hub-dependencies.bicep deleted file mode 100644 index eeabee7..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/ai/hub-dependencies.bicep +++ /dev/null @@ -1,170 +0,0 @@ -param location string = resourceGroup().location -param tags object = {} - -@description('Name of the key vault') -param keyVaultName string -@description('Name of the storage account') -param storageAccountName string -@description('Name of the OpenAI cognitive services') -param openAiName string -@description('Array of OpenAI model deployments') -param openAiModelDeployments array = [] -@description('Name of the Log Analytics workspace') -param logAnalyticsName string = '' -@description('Name of the Application Insights instance') -param applicationInsightsName string = '' -@description('Name of the container registry') -param containerRegistryName string = '' -@description('Name of the Azure Cognitive Search service') -param searchServiceName string = '' - -module keyVault '../security/keyvault.bicep' = { - name: 'keyvault' - params: { - location: location - tags: tags - name: keyVaultName - } -} - -module storageAccount '../storage/storage-account.bicep' = { - name: 'storageAccount' - params: { - location: location - tags: tags - name: storageAccountName - containers: [ - { - name: 'default' - } - ] - files: [ - { - name: 'default' - } - ] - queues: [ - { - name: 'default' - } - ] - tables: [ - { - name: 'default' - } - ] - corsRules: [ - { - allowedOrigins: [ - 'https://mlworkspace.azure.ai' - 'https://ml.azure.com' - 'https://*.ml.azure.com' - 'https://ai.azure.com' - 'https://*.ai.azure.com' - 'https://mlworkspacecanary.azure.ai' - 'https://mlworkspace.azureml-test.net' - ] - allowedMethods: [ - 'GET' - 'HEAD' - 'POST' - 'PUT' - 'DELETE' - 'OPTIONS' - 'PATCH' - ] - maxAgeInSeconds: 1800 - exposedHeaders: [ - '*' - ] - allowedHeaders: [ - '*' - ] - } - ] - deleteRetentionPolicy: { - allowPermanentDelete: false - enabled: false - } - shareDeleteRetentionPolicy: { - enabled: true - days: 7 - } - } -} - -module logAnalytics '../monitor/loganalytics.bicep' = - if (!empty(logAnalyticsName)) { - name: 'logAnalytics' - params: { - location: location - tags: tags - name: logAnalyticsName - } - } - -module applicationInsights '../monitor/applicationinsights.bicep' = - if (!empty(applicationInsightsName) && !empty(logAnalyticsName)) { - name: 'applicationInsights' - params: { - location: location - tags: tags - name: applicationInsightsName - logAnalyticsWorkspaceId: !empty(logAnalyticsName) ? logAnalytics.outputs.id : '' - } - } - -module containerRegistry '../host/container-registry.bicep' = - if (!empty(containerRegistryName)) { - name: 'containerRegistry' - params: { - location: location - tags: tags - name: containerRegistryName - } - } - -module cognitiveServices '../ai/cognitiveservices.bicep' = { - name: 'cognitiveServices' - params: { - location: location - tags: tags - name: openAiName - kind: 'AIServices' - deployments: openAiModelDeployments - } -} - -module searchService '../search/search-services.bicep' = - if (!empty(searchServiceName)) { - name: 'searchService' - params: { - location: location - tags: tags - name: searchServiceName - } - } - -output keyVaultId string = keyVault.outputs.id -output keyVaultName string = keyVault.outputs.name -output keyVaultEndpoint string = keyVault.outputs.endpoint - -output storageAccountId string = storageAccount.outputs.id -output storageAccountName string = storageAccount.outputs.name - -output containerRegistryId string = !empty(containerRegistryName) ? containerRegistry.outputs.id : '' -output containerRegistryName string = !empty(containerRegistryName) ? containerRegistry.outputs.name : '' -output containerRegistryEndpoint string = !empty(containerRegistryName) ? containerRegistry.outputs.loginServer : '' - -output applicationInsightsId string = !empty(applicationInsightsName) ? applicationInsights.outputs.id : '' -output applicationInsightsName string = !empty(applicationInsightsName) ? applicationInsights.outputs.name : '' -output logAnalyticsWorkspaceId string = !empty(logAnalyticsName) ? logAnalytics.outputs.id : '' -output logAnalyticsWorkspaceName string = !empty(logAnalyticsName) ? logAnalytics.outputs.name : '' - -output openAiId string = cognitiveServices.outputs.id -output openAiName string = cognitiveServices.outputs.name -output openAiEndpoint string = cognitiveServices.outputs.endpoints['OpenAI Language Model Instance API'] - -output searchServiceId string = !empty(searchServiceName) ? searchService.outputs.id : '' -output searchServiceName string = !empty(searchServiceName) ? searchService.outputs.name : '' -output searchServiceEndpoint string = !empty(searchServiceName) ? searchService.outputs.endpoint : '' diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/ai/hub.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/ai/hub.bicep deleted file mode 100644 index 576d13c..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/ai/hub.bicep +++ /dev/null @@ -1,113 +0,0 @@ -@description('The AI Studio Hub Resource name') -param name string -@description('The display name of the AI Studio Hub Resource') -param displayName string = name -@description('The storage account ID to use for the AI Studio Hub Resource') -param storageAccountId string -@description('The key vault ID to use for the AI Studio Hub Resource') -param keyVaultId string -@description('The application insights ID to use for the AI Studio Hub Resource') -param applicationInsightsId string = '' -@description('The container registry ID to use for the AI Studio Hub Resource') -param containerRegistryId string = '' -@description('The OpenAI Cognitive Services account name to use for the AI Studio Hub Resource') -param openAiName string -@description('The OpenAI Cognitive Services account connection name to use for the AI Studio Hub Resource') -param openAiConnectionName string -@description('The Azure Cognitive Search service name to use for the AI Studio Hub Resource') -param aiSearchName string = '' -@description('The Azure Cognitive Search service connection name to use for the AI Studio Hub Resource') -param aiSearchConnectionName string - -@description('The SKU name to use for the AI Studio Hub Resource') -param skuName string = 'Basic' -@description('The SKU tier to use for the AI Studio Hub Resource') -@allowed(['Basic', 'Free', 'Premium', 'Standard']) -param skuTier string = 'Basic' -@description('The public network access setting to use for the AI Studio Hub Resource') -@allowed(['Enabled','Disabled']) -param publicNetworkAccess string = 'Enabled' - -param location string = resourceGroup().location -param tags object = {} - -resource hub 'Microsoft.MachineLearningServices/workspaces@2024-01-01-preview' = { - name: name - location: location - tags: tags - sku: { - name: skuName - tier: skuTier - } - kind: 'Hub' - identity: { - type: 'SystemAssigned' - } - properties: { - friendlyName: displayName - storageAccount: storageAccountId - keyVault: keyVaultId - applicationInsights: !empty(applicationInsightsId) ? applicationInsightsId : null - containerRegistry: !empty(containerRegistryId) ? containerRegistryId : null - hbiWorkspace: false - managedNetwork: { - isolationMode: 'Disabled' - } - v1LegacyMode: false - publicNetworkAccess: publicNetworkAccess - } - - resource contentSafetyDefaultEndpoint 'endpoints' = { - name: 'Azure.ContentSafety' - properties: { - name: 'Azure.ContentSafety' - endpointType: 'Azure.ContentSafety' - associatedResourceId: openAi.id - } - } - - resource openAiConnection 'connections' = { - name: openAiConnectionName - properties: { - category: 'AzureOpenAI' - authType: 'ApiKey' - isSharedToAll: true - target: openAi.properties.endpoints['OpenAI Language Model Instance API'] - metadata: { - ApiVersion: '2023-07-01-preview' - ApiType: 'azure' - ResourceId: openAi.id - } - credentials: { - key: openAi.listKeys().key1 - } - } - } - - resource searchConnection 'connections' = - if (!empty(aiSearchName)) { - name: aiSearchConnectionName - properties: { - category: 'CognitiveSearch' - authType: 'ApiKey' - isSharedToAll: true - target: 'https://${search.name}.search.windows.net/' - credentials: { - key: !empty(aiSearchName) ? search.listAdminKeys().primaryKey : '' - } - } - } -} - -resource openAi 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = { - name: openAiName -} - -resource search 'Microsoft.Search/searchServices@2021-04-01-preview' existing = - if (!empty(aiSearchName)) { - name: aiSearchName - } - -output name string = hub.name -output id string = hub.id -output principalId string = hub.identity.principalId diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/ai/project.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/ai/project.bicep deleted file mode 100644 index 78b0d52..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/ai/project.bicep +++ /dev/null @@ -1,75 +0,0 @@ -@description('The AI Studio Hub Resource name') -param name string -@description('The display name of the AI Studio Hub Resource') -param displayName string = name -@description('The name of the AI Studio Hub Resource where this project should be created') -param hubName string -@description('The name of the key vault resource to grant access to the project') -param keyVaultName string - -@description('The SKU name to use for the AI Studio Hub Resource') -param skuName string = 'Basic' -@description('The SKU tier to use for the AI Studio Hub Resource') -@allowed(['Basic', 'Free', 'Premium', 'Standard']) -param skuTier string = 'Basic' -@description('The public network access setting to use for the AI Studio Hub Resource') -@allowed(['Enabled','Disabled']) -param publicNetworkAccess string = 'Enabled' - -param location string = resourceGroup().location -param tags object = {} - -resource project 'Microsoft.MachineLearningServices/workspaces@2024-01-01-preview' = { - name: name - location: location - tags: tags - sku: { - name: skuName - tier: skuTier - } - kind: 'Project' - identity: { - type: 'SystemAssigned' - } - properties: { - friendlyName: displayName - hbiWorkspace: false - v1LegacyMode: false - publicNetworkAccess: publicNetworkAccess - hubResourceId: hub.id - } -} - -module keyVaultAccess '../security/keyvault-access.bicep' = { - name: 'keyvault-access' - params: { - keyVaultName: keyVaultName - principalId: project.identity.principalId - } -} - -module mlServiceRoleDataScientist '../security/role.bicep' = { - name: 'ml-service-role-data-scientist' - params: { - principalId: project.identity.principalId - roleDefinitionId: 'f6c7c914-8db3-469d-8ca1-694a8f32e121' - principalType: 'ServicePrincipal' - } -} - -module mlServiceRoleSecretsReader '../security/role.bicep' = { - name: 'ml-service-role-secrets-reader' - params: { - principalId: project.identity.principalId - roleDefinitionId: 'ea01e6af-a1c1-4350-9563-ad00f8c72ec5' - principalType: 'ServicePrincipal' - } -} - -resource hub 'Microsoft.MachineLearningServices/workspaces@2024-01-01-preview' existing = { - name: hubName -} - -output id string = project.id -output name string = project.name -output principalId string = project.identity.principalId diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/config/configstore.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/config/configstore.bicep deleted file mode 100644 index 96818f1..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/config/configstore.bicep +++ /dev/null @@ -1,48 +0,0 @@ -metadata description = 'Creates an Azure App Configuration store.' - -@description('The name for the Azure App Configuration store') -param name string - -@description('The Azure region/location for the Azure App Configuration store') -param location string = resourceGroup().location - -@description('Custom tags to apply to the Azure App Configuration store') -param tags object = {} - -@description('Specifies the names of the key-value resources. The name is a combination of key and label with $ as delimiter. The label is optional.') -param keyValueNames array = [] - -@description('Specifies the values of the key-value resources.') -param keyValueValues array = [] - -@description('The principal ID to grant access to the Azure App Configuration store') -param principalId string - -resource configStore 'Microsoft.AppConfiguration/configurationStores@2023-03-01' = { - name: name - location: location - sku: { - name: 'standard' - } - tags: tags -} - -resource configStoreKeyValue 'Microsoft.AppConfiguration/configurationStores/keyValues@2023-03-01' = [for (item, i) in keyValueNames: { - parent: configStore - name: item - properties: { - value: keyValueValues[i] - tags: tags - } -}] - -module configStoreAccess '../security/configstore-access.bicep' = { - name: 'app-configuration-access' - params: { - configStoreName: name - principalId: principalId - } - dependsOn: [configStore] -} - -output endpoint string = configStore.properties.endpoint diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/cosmos-account.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/cosmos-account.bicep deleted file mode 100644 index c16b229..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/cosmos-account.bicep +++ /dev/null @@ -1,50 +0,0 @@ -metadata description = 'Creates an Azure Cosmos DB account.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' -param keyVaultName string - -@allowed([ 'GlobalDocumentDB', 'MongoDB', 'Parse' ]) -param kind string - -resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = { - name: name - kind: kind - location: location - tags: tags - properties: { - consistencyPolicy: { defaultConsistencyLevel: 'Session' } - locations: [ - { - locationName: location - failoverPriority: 0 - isZoneRedundant: false - } - ] - databaseAccountOfferType: 'Standard' - enableAutomaticFailover: false - enableMultipleWriteLocations: false - apiProperties: (kind == 'MongoDB') ? { serverVersion: '4.2' } : {} - capabilities: [ { name: 'EnableServerless' } ] - minimalTlsVersion: 'Tls12' - } -} - -resource cosmosConnectionString 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: connectionStringKey - properties: { - value: cosmos.listConnectionStrings().connectionStrings[0].connectionString - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} - -output connectionStringKey string = connectionStringKey -output endpoint string = cosmos.properties.documentEndpoint -output id string = cosmos.id -output name string = cosmos.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep deleted file mode 100644 index 4aafbf3..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep +++ /dev/null @@ -1,23 +0,0 @@ -metadata description = 'Creates an Azure Cosmos DB for MongoDB account.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param keyVaultName string -param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' - -module cosmos '../../cosmos/cosmos-account.bicep' = { - name: 'cosmos-account' - params: { - name: name - location: location - connectionStringKey: connectionStringKey - keyVaultName: keyVaultName - kind: 'MongoDB' - tags: tags - } -} - -output connectionStringKey string = cosmos.outputs.connectionStringKey -output endpoint string = cosmos.outputs.endpoint -output id string = cosmos.outputs.id diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep deleted file mode 100644 index 2a67057..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep +++ /dev/null @@ -1,47 +0,0 @@ -metadata description = 'Creates an Azure Cosmos DB for MongoDB account with a database.' -param accountName string -param databaseName string -param location string = resourceGroup().location -param tags object = {} - -param collections array = [] -param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' -param keyVaultName string - -module cosmos 'cosmos-mongo-account.bicep' = { - name: 'cosmos-mongo-account' - params: { - name: accountName - location: location - keyVaultName: keyVaultName - tags: tags - connectionStringKey: connectionStringKey - } -} - -resource database 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2022-08-15' = { - name: '${accountName}/${databaseName}' - tags: tags - properties: { - resource: { id: databaseName } - } - - resource list 'collections' = [for collection in collections: { - name: collection.name - properties: { - resource: { - id: collection.id - shardKey: { _id: collection.shardKey } - indexes: [ { key: { keys: [ collection.indexKey ] } } ] - } - } - }] - - dependsOn: [ - cosmos - ] -} - -output connectionStringKey string = connectionStringKey -output databaseName string = databaseName -output endpoint string = cosmos.outputs.endpoint diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-account.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-account.bicep deleted file mode 100644 index 8431135..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-account.bicep +++ /dev/null @@ -1,22 +0,0 @@ -metadata description = 'Creates an Azure Cosmos DB for NoSQL account.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param keyVaultName string - -module cosmos '../../cosmos/cosmos-account.bicep' = { - name: 'cosmos-account' - params: { - name: name - location: location - tags: tags - keyVaultName: keyVaultName - kind: 'GlobalDocumentDB' - } -} - -output connectionStringKey string = cosmos.outputs.connectionStringKey -output endpoint string = cosmos.outputs.endpoint -output id string = cosmos.outputs.id -output name string = cosmos.outputs.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-db.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-db.bicep deleted file mode 100644 index 265880d..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-db.bicep +++ /dev/null @@ -1,74 +0,0 @@ -metadata description = 'Creates an Azure Cosmos DB for NoSQL account with a database.' -param accountName string -param databaseName string -param location string = resourceGroup().location -param tags object = {} - -param containers array = [] -param keyVaultName string -param principalIds array = [] - -module cosmos 'cosmos-sql-account.bicep' = { - name: 'cosmos-sql-account' - params: { - name: accountName - location: location - tags: tags - keyVaultName: keyVaultName - } -} - -resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2022-05-15' = { - name: '${accountName}/${databaseName}' - properties: { - resource: { id: databaseName } - } - - resource list 'containers' = [for container in containers: { - name: container.name - properties: { - resource: { - id: container.id - partitionKey: { paths: [ container.partitionKey ] } - } - options: {} - } - }] - - dependsOn: [ - cosmos - ] -} - -module roleDefinition 'cosmos-sql-role-def.bicep' = { - name: 'cosmos-sql-role-definition' - params: { - accountName: accountName - } - dependsOn: [ - cosmos - database - ] -} - -// We need batchSize(1) here because sql role assignments have to be done sequentially -@batchSize(1) -module userRole 'cosmos-sql-role-assign.bicep' = [for principalId in principalIds: if (!empty(principalId)) { - name: 'cosmos-sql-user-role-${uniqueString(principalId)}' - params: { - accountName: accountName - roleDefinitionId: roleDefinition.outputs.id - principalId: principalId - } - dependsOn: [ - cosmos - database - ] -}] - -output accountId string = cosmos.outputs.id -output accountName string = cosmos.outputs.name -output connectionStringKey string = cosmos.outputs.connectionStringKey -output databaseName string = databaseName -output endpoint string = cosmos.outputs.endpoint -output roleDefinitionId string = roleDefinition.outputs.id diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep deleted file mode 100644 index 3949efe..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep +++ /dev/null @@ -1,19 +0,0 @@ -metadata description = 'Creates a SQL role assignment under an Azure Cosmos DB account.' -param accountName string - -param roleDefinitionId string -param principalId string = '' - -resource role 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-05-15' = { - parent: cosmos - name: guid(roleDefinitionId, principalId, cosmos.id) - properties: { - principalId: principalId - roleDefinitionId: roleDefinitionId - scope: cosmos.id - } -} - -resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = { - name: accountName -} diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep deleted file mode 100644 index 778d6dc..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep +++ /dev/null @@ -1,30 +0,0 @@ -metadata description = 'Creates a SQL role definition under an Azure Cosmos DB account.' -param accountName string - -resource roleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2022-08-15' = { - parent: cosmos - name: guid(cosmos.id, accountName, 'sql-role') - properties: { - assignableScopes: [ - cosmos.id - ] - permissions: [ - { - dataActions: [ - 'Microsoft.DocumentDB/databaseAccounts/readMetadata' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' - ] - notDataActions: [] - } - ] - roleName: 'Reader Writer' - type: 'CustomRole' - } -} - -resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = { - name: accountName -} - -output id string = roleDefinition.id diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/database/mysql/flexibleserver.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/database/mysql/flexibleserver.bicep deleted file mode 100644 index 8319f1c..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/database/mysql/flexibleserver.bicep +++ /dev/null @@ -1,65 +0,0 @@ -metadata description = 'Creates an Azure Database for MySQL - Flexible Server.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object -param storage object -param administratorLogin string -@secure() -param administratorLoginPassword string -param highAvailabilityMode string = 'Disabled' -param databaseNames array = [] -param allowAzureIPsFirewall bool = false -param allowAllIPsFirewall bool = false -param allowedSingleIPs array = [] - -// MySQL version -param version string - -resource mysqlServer 'Microsoft.DBforMySQL/flexibleServers@2023-06-30' = { - location: location - tags: tags - name: name - sku: sku - properties: { - version: version - administratorLogin: administratorLogin - administratorLoginPassword: administratorLoginPassword - storage: storage - highAvailability: { - mode: highAvailabilityMode - } - } - - resource database 'databases' = [for name in databaseNames: { - name: name - }] - - resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) { - name: 'allow-all-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '255.255.255.255' - } - } - - resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) { - name: 'allow-all-azure-internal-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } - } - - resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: { - name: 'allow-single-${replace(ip, '.', '')}' - properties: { - startIpAddress: ip - endIpAddress: ip - } - }] - -} - -output MYSQL_DOMAIN_NAME string = mysqlServer.properties.fullyQualifiedDomainName diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/database/postgresql/flexibleserver.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/database/postgresql/flexibleserver.bicep deleted file mode 100644 index 7e26b1a..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/database/postgresql/flexibleserver.bicep +++ /dev/null @@ -1,65 +0,0 @@ -metadata description = 'Creates an Azure Database for PostgreSQL - Flexible Server.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object -param storage object -param administratorLogin string -@secure() -param administratorLoginPassword string -param databaseNames array = [] -param allowAzureIPsFirewall bool = false -param allowAllIPsFirewall bool = false -param allowedSingleIPs array = [] - -// PostgreSQL version -param version string - -// Latest official version 2022-12-01 does not have Bicep types available -resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = { - location: location - tags: tags - name: name - sku: sku - properties: { - version: version - administratorLogin: administratorLogin - administratorLoginPassword: administratorLoginPassword - storage: storage - highAvailability: { - mode: 'Disabled' - } - } - - resource database 'databases' = [for name in databaseNames: { - name: name - }] - - resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) { - name: 'allow-all-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '255.255.255.255' - } - } - - resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) { - name: 'allow-all-azure-internal-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } - } - - resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: { - name: 'allow-single-${replace(ip, '.', '')}' - properties: { - startIpAddress: ip - endIpAddress: ip - } - }] - -} - -output POSTGRES_DOMAIN_NAME string = postgresServer.properties.fullyQualifiedDomainName diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/database/sqlserver/sqlserver.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/database/sqlserver/sqlserver.bicep deleted file mode 100644 index 84f2cc2..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/database/sqlserver/sqlserver.bicep +++ /dev/null @@ -1,130 +0,0 @@ -metadata description = 'Creates an Azure SQL Server instance.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param appUser string = 'appUser' -param databaseName string -param keyVaultName string -param sqlAdmin string = 'sqlAdmin' -param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING' - -@secure() -param sqlAdminPassword string -@secure() -param appUserPassword string - -resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = { - name: name - location: location - tags: tags - properties: { - version: '12.0' - minimalTlsVersion: '1.2' - publicNetworkAccess: 'Enabled' - administratorLogin: sqlAdmin - administratorLoginPassword: sqlAdminPassword - } - - resource database 'databases' = { - name: databaseName - location: location - } - - resource firewall 'firewallRules' = { - name: 'Azure Services' - properties: { - // Allow all clients - // Note: range [0.0.0.0-0.0.0.0] means "allow all Azure-hosted clients only". - // This is not sufficient, because we also want to allow direct access from developer machine, for debugging purposes. - startIpAddress: '0.0.0.1' - endIpAddress: '255.255.255.254' - } - } -} - -resource sqlDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { - name: '${name}-deployment-script' - location: location - kind: 'AzureCLI' - properties: { - azCliVersion: '2.37.0' - retentionInterval: 'PT1H' // Retain the script resource for 1 hour after it ends running - timeout: 'PT5M' // Five minutes - cleanupPreference: 'OnSuccess' - environmentVariables: [ - { - name: 'APPUSERNAME' - value: appUser - } - { - name: 'APPUSERPASSWORD' - secureValue: appUserPassword - } - { - name: 'DBNAME' - value: databaseName - } - { - name: 'DBSERVER' - value: sqlServer.properties.fullyQualifiedDomainName - } - { - name: 'SQLCMDPASSWORD' - secureValue: sqlAdminPassword - } - { - name: 'SQLADMIN' - value: sqlAdmin - } - ] - - scriptContent: ''' -wget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8.1-linux-x64.tar.bz2 -tar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C . - -cat < ./initDb.sql -drop user if exists ${APPUSERNAME} -go -create user ${APPUSERNAME} with password = '${APPUSERPASSWORD}' -go -alter role db_owner add member ${APPUSERNAME} -go -SCRIPT_END - -./sqlcmd -S ${DBSERVER} -d ${DBNAME} -U ${SQLADMIN} -i ./initDb.sql - ''' - } -} - -resource sqlAdminPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: 'sqlAdminPassword' - properties: { - value: sqlAdminPassword - } -} - -resource appUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: 'appUserPassword' - properties: { - value: appUserPassword - } -} - -resource sqlAzureConnectionStringSercret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: connectionStringKey - properties: { - value: '${connectionString}; Password=${appUserPassword}' - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} - -var connectionString = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlServer::database.name}; User=${appUser}' -output connectionStringKey string = connectionStringKey -output databaseName string = sqlServer::database.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/gateway/apim.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/gateway/apim.bicep deleted file mode 100644 index be7464f..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/gateway/apim.bicep +++ /dev/null @@ -1,79 +0,0 @@ -metadata description = 'Creates an Azure API Management instance.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('The email address of the owner of the service') -@minLength(1) -param publisherEmail string = 'noreply@microsoft.com' - -@description('The name of the owner of the service') -@minLength(1) -param publisherName string = 'n/a' - -@description('The pricing tier of this API Management service') -@allowed([ - 'Consumption' - 'Developer' - 'Standard' - 'Premium' -]) -param sku string = 'Consumption' - -@description('The instance size of this API Management service.') -@allowed([ 0, 1, 2 ]) -param skuCount int = 0 - -@description('Azure Application Insights Name') -param applicationInsightsName string - -resource apimService 'Microsoft.ApiManagement/service@2021-08-01' = { - name: name - location: location - tags: union(tags, { 'azd-service-name': name }) - sku: { - name: sku - capacity: (sku == 'Consumption') ? 0 : ((sku == 'Developer') ? 1 : skuCount) - } - properties: { - publisherEmail: publisherEmail - publisherName: publisherName - // Custom properties are not supported for Consumption SKU - customProperties: sku == 'Consumption' ? {} : { - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_GCM_SHA256': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA256': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA256': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TripleDes168': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Ssl30': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30': 'false' - } - } -} - -resource apimLogger 'Microsoft.ApiManagement/service/loggers@2021-12-01-preview' = if (!empty(applicationInsightsName)) { - name: 'app-insights-logger' - parent: apimService - properties: { - credentials: { - instrumentationKey: applicationInsights.properties.InstrumentationKey - } - description: 'Logger to Azure Application Insights' - isBuffered: false - loggerType: 'applicationInsights' - resourceId: applicationInsights.id - } -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { - name: applicationInsightsName -} - -output apimServiceName string = apimService.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/ai-environment.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/ai-environment.bicep deleted file mode 100644 index d03675f..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/ai-environment.bicep +++ /dev/null @@ -1,110 +0,0 @@ -@minLength(1) -@description('Primary location for all resources') -param location string - -@description('The AI Hub resource name.') -param hubName string -@description('The AI Project resource name.') -param projectName string -@description('The Key Vault resource name.') -param keyVaultName string -@description('The Storage Account resource name.') -param storageAccountName string -@description('The Open AI resource name.') -param openAiName string -@description('The Open AI connection name.') -param openAiConnectionName string -@description('The Open AI model deployments.') -param openAiModelDeployments array = [] -@description('The Log Analytics resource name.') -param logAnalyticsName string = '' -@description('The Application Insights resource name.') -param applicationInsightsName string = '' -@description('The Container Registry resource name.') -param containerRegistryName string = '' -@description('The Azure Search resource name.') -param searchServiceName string = '' -@description('The Azure Search connection name.') -param searchConnectionName string = '' -param tags object = {} - -module hubDependencies '../ai/hub-dependencies.bicep' = { - name: 'hubDependencies' - params: { - location: location - tags: tags - keyVaultName: keyVaultName - storageAccountName: storageAccountName - containerRegistryName: containerRegistryName - applicationInsightsName: applicationInsightsName - logAnalyticsName: logAnalyticsName - openAiName: openAiName - openAiModelDeployments: openAiModelDeployments - searchServiceName: searchServiceName - } -} - -module hub '../ai/hub.bicep' = { - name: 'hub' - params: { - location: location - tags: tags - name: hubName - displayName: hubName - keyVaultId: hubDependencies.outputs.keyVaultId - storageAccountId: hubDependencies.outputs.storageAccountId - containerRegistryId: hubDependencies.outputs.containerRegistryId - applicationInsightsId: hubDependencies.outputs.applicationInsightsId - openAiName: hubDependencies.outputs.openAiName - openAiConnectionName: openAiConnectionName - aiSearchName: hubDependencies.outputs.searchServiceName - aiSearchConnectionName: searchConnectionName - } -} - -module project '../ai/project.bicep' = { - name: 'project' - params: { - location: location - tags: tags - name: projectName - displayName: projectName - hubName: hub.outputs.name - keyVaultName: hubDependencies.outputs.keyVaultName - } -} - -// Outputs -// Resource Group -output resourceGroupName string = resourceGroup().name - -// Hub -output hubName string = hub.outputs.name -output hubPrincipalId string = hub.outputs.principalId - -// Project -output projectName string = project.outputs.name -output projectPrincipalId string = project.outputs.principalId - -// Key Vault -output keyVaultName string = hubDependencies.outputs.keyVaultName -output keyVaultEndpoint string = hubDependencies.outputs.keyVaultEndpoint - -// Application Insights -output applicationInsightsName string = hubDependencies.outputs.applicationInsightsName -output logAnalyticsWorkspaceName string = hubDependencies.outputs.logAnalyticsWorkspaceName - -// Container Registry -output containerRegistryName string = hubDependencies.outputs.containerRegistryName -output containerRegistryEndpoint string = hubDependencies.outputs.containerRegistryEndpoint - -// Storage Account -output storageAccountName string = hubDependencies.outputs.storageAccountName - -// Open AI -output openAiName string = hubDependencies.outputs.openAiName -output openAiEndpoint string = hubDependencies.outputs.openAiEndpoint - -// Search -output searchServiceName string = hubDependencies.outputs.searchServiceName -output searchServiceEndpoint string = hubDependencies.outputs.searchServiceEndpoint diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/aks-agent-pool.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/aks-agent-pool.bicep deleted file mode 100644 index 9c76435..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/aks-agent-pool.bicep +++ /dev/null @@ -1,18 +0,0 @@ -metadata description = 'Adds an agent pool to an Azure Kubernetes Service (AKS) cluster.' -param clusterName string - -@description('The agent pool name') -param name string - -@description('The agent pool configuration') -param config object - -resource aksCluster 'Microsoft.ContainerService/managedClusters@2023-10-02-preview' existing = { - name: clusterName -} - -resource nodePool 'Microsoft.ContainerService/managedClusters/agentPools@2023-10-02-preview' = { - parent: aksCluster - name: name - properties: config -} diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/aks-managed-cluster.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/aks-managed-cluster.bicep deleted file mode 100644 index de562a6..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/aks-managed-cluster.bicep +++ /dev/null @@ -1,140 +0,0 @@ -metadata description = 'Creates an Azure Kubernetes Service (AKS) cluster with a system agent pool.' -@description('The name for the AKS managed cluster') -param name string - -@description('The name of the resource group for the managed resources of the AKS cluster') -param nodeResourceGroupName string = '' - -@description('The Azure region/location for the AKS resources') -param location string = resourceGroup().location - -@description('Custom tags to apply to the AKS resources') -param tags object = {} - -@description('Kubernetes Version') -param kubernetesVersion string = '1.27.7' - -@description('Whether RBAC is enabled for local accounts') -param enableRbac bool = true - -// Add-ons -@description('Whether web app routing (preview) add-on is enabled') -param webAppRoutingAddon bool = true - -// AAD Integration -@description('Enable Azure Active Directory integration') -param enableAad bool = false - -@description('Enable RBAC using AAD') -param enableAzureRbac bool = false - -@description('The Tenant ID associated to the Azure Active Directory') -param aadTenantId string = tenant().tenantId - -@description('The load balancer SKU to use for ingress into the AKS cluster') -@allowed([ 'basic', 'standard' ]) -param loadBalancerSku string = 'standard' - -@description('Network plugin used for building the Kubernetes network.') -@allowed([ 'azure', 'kubenet', 'none' ]) -param networkPlugin string = 'azure' - -@description('Network policy used for building the Kubernetes network.') -@allowed([ 'azure', 'calico' ]) -param networkPolicy string = 'azure' - -@description('If set to true, getting static credentials will be disabled for this cluster.') -param disableLocalAccounts bool = false - -@description('The managed cluster SKU.') -@allowed([ 'Free', 'Paid', 'Standard' ]) -param sku string = 'Free' - -@description('Configuration of AKS add-ons') -param addOns object = {} - -@description('The log analytics workspace id used for logging & monitoring') -param workspaceId string = '' - -@description('The node pool configuration for the System agent pool') -param systemPoolConfig object - -@description('The DNS prefix to associate with the AKS cluster') -param dnsPrefix string = '' - -resource aks 'Microsoft.ContainerService/managedClusters@2023-10-02-preview' = { - name: name - location: location - tags: tags - identity: { - type: 'SystemAssigned' - } - sku: { - name: 'Base' - tier: sku - } - properties: { - nodeResourceGroup: !empty(nodeResourceGroupName) ? nodeResourceGroupName : 'rg-mc-${name}' - kubernetesVersion: kubernetesVersion - dnsPrefix: empty(dnsPrefix) ? '${name}-dns' : dnsPrefix - enableRBAC: enableRbac - aadProfile: enableAad ? { - managed: true - enableAzureRBAC: enableAzureRbac - tenantID: aadTenantId - } : null - agentPoolProfiles: [ - systemPoolConfig - ] - networkProfile: { - loadBalancerSku: loadBalancerSku - networkPlugin: networkPlugin - networkPolicy: networkPolicy - } - disableLocalAccounts: disableLocalAccounts && enableAad - addonProfiles: addOns - ingressProfile: { - webAppRouting: { - enabled: webAppRoutingAddon - } - } - } -} - -var aksDiagCategories = [ - 'cluster-autoscaler' - 'kube-controller-manager' - 'kube-audit-admin' - 'guard' -] - -// TODO: Update diagnostics to be its own module -// Blocking issue: https://github.com/Azure/bicep/issues/622 -// Unable to pass in a `resource` scope or unable to use string interpolation in resource types -resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { - name: 'aks-diagnostics' - scope: aks - properties: { - workspaceId: workspaceId - logs: [for category in aksDiagCategories: { - category: category - enabled: true - }] - metrics: [ - { - category: 'AllMetrics' - enabled: true - } - ] - } -} - -@description('The resource name of the AKS cluster') -output clusterName string = aks.name - -@description('The AKS cluster identity') -output clusterIdentity object = { - clientId: aks.properties.identityProfile.kubeletidentity.clientId - objectId: aks.properties.identityProfile.kubeletidentity.objectId - resourceId: aks.properties.identityProfile.kubeletidentity.resourceId -} diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/aks.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/aks.bicep deleted file mode 100644 index 15bb7cb..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/aks.bicep +++ /dev/null @@ -1,285 +0,0 @@ -metadata description = 'Creates an Azure Kubernetes Service (AKS) cluster with a system agent pool as well as an additional user agent pool.' -@description('The name for the AKS managed cluster') -param name string - -@description('The name for the Azure container registry (ACR)') -param containerRegistryName string - -@description('The name of the connected log analytics workspace') -param logAnalyticsName string = '' - -@description('The name of the keyvault to grant access') -param keyVaultName string - -@description('The Azure region/location for the AKS resources') -param location string = resourceGroup().location - -@description('Custom tags to apply to the AKS resources') -param tags object = {} - -@description('AKS add-ons configuration') -param addOns object = { - azurePolicy: { - enabled: true - config: { - version: 'v2' - } - } - keyVault: { - enabled: true - config: { - enableSecretRotation: 'true' - rotationPollInterval: '2m' - } - } - openServiceMesh: { - enabled: false - config: {} - } - omsAgent: { - enabled: true - config: {} - } - applicationGateway: { - enabled: false - config: {} - } -} - -@description('The managed cluster SKU.') -@allowed([ 'Free', 'Paid', 'Standard' ]) -param sku string = 'Free' - -@description('The load balancer SKU to use for ingress into the AKS cluster') -@allowed([ 'basic', 'standard' ]) -param loadBalancerSku string = 'standard' - -@description('Network plugin used for building the Kubernetes network.') -@allowed([ 'azure', 'kubenet', 'none' ]) -param networkPlugin string = 'azure' - -@description('Network policy used for building the Kubernetes network.') -@allowed([ 'azure', 'calico' ]) -param networkPolicy string = 'azure' - -@description('The DNS prefix to associate with the AKS cluster') -param dnsPrefix string = '' - -@description('The name of the resource group for the managed resources of the AKS cluster') -param nodeResourceGroupName string = '' - -@allowed([ - 'CostOptimised' - 'Standard' - 'HighSpec' - 'Custom' -]) -@description('The System Pool Preset sizing') -param systemPoolType string = 'CostOptimised' - -@allowed([ - '' - 'CostOptimised' - 'Standard' - 'HighSpec' - 'Custom' -]) -@description('The User Pool Preset sizing') -param agentPoolType string = '' - -// Configure system / user agent pools -@description('Custom configuration of system node pool') -param systemPoolConfig object = {} -@description('Custom configuration of user node pool') -param agentPoolConfig object = {} - -@description('Id of the user or app to assign application roles') -param principalId string = '' - -@description('The type of principal to assign application roles') -@allowed(['Device','ForeignGroup','Group','ServicePrincipal','User']) -param principalType string = 'User' - -@description('Kubernetes Version') -param kubernetesVersion string = '1.27' - -@description('The Tenant ID associated to the Azure Active Directory') -param aadTenantId string = tenant().tenantId - -@description('Whether RBAC is enabled for local accounts') -param enableRbac bool = true - -@description('If set to true, getting static credentials will be disabled for this cluster.') -param disableLocalAccounts bool = false - -@description('Enable RBAC using AAD') -param enableAzureRbac bool = false - -// Add-ons -@description('Whether web app routing (preview) add-on is enabled') -param webAppRoutingAddon bool = true - -// Configure AKS add-ons -var omsAgentConfig = (!empty(logAnalyticsName) && !empty(addOns.omsAgent) && addOns.omsAgent.enabled) ? union( - addOns.omsAgent, - { - config: { - logAnalyticsWorkspaceResourceID: logAnalytics.id - } - } -) : {} - -var addOnsConfig = union( - (!empty(addOns.azurePolicy) && addOns.azurePolicy.enabled) ? { azurepolicy: addOns.azurePolicy } : {}, - (!empty(addOns.keyVault) && addOns.keyVault.enabled) ? { azureKeyvaultSecretsProvider: addOns.keyVault } : {}, - (!empty(addOns.openServiceMesh) && addOns.openServiceMesh.enabled) ? { openServiceMesh: addOns.openServiceMesh } : {}, - (!empty(addOns.omsAgent) && addOns.omsAgent.enabled) ? { omsagent: omsAgentConfig } : {}, - (!empty(addOns.applicationGateway) && addOns.applicationGateway.enabled) ? { ingressApplicationGateway: addOns.applicationGateway } : {} -) - -// Link to existing log analytics workspace when available -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' existing = if (!empty(logAnalyticsName)) { - name: logAnalyticsName -} - -var systemPoolSpec = !empty(systemPoolConfig) ? systemPoolConfig : nodePoolPresets[systemPoolType] - -// Create the primary AKS cluster resources and system node pool -module managedCluster 'aks-managed-cluster.bicep' = { - name: 'managed-cluster' - params: { - name: name - location: location - tags: tags - systemPoolConfig: union( - { name: 'npsystem', mode: 'System' }, - nodePoolBase, - systemPoolSpec - ) - nodeResourceGroupName: nodeResourceGroupName - sku: sku - dnsPrefix: dnsPrefix - kubernetesVersion: kubernetesVersion - addOns: addOnsConfig - workspaceId: !empty(logAnalyticsName) ? logAnalytics.id : '' - enableAad: enableAzureRbac && aadTenantId != '' - disableLocalAccounts: disableLocalAccounts - aadTenantId: aadTenantId - enableRbac: enableRbac - enableAzureRbac: enableAzureRbac - webAppRoutingAddon: webAppRoutingAddon - loadBalancerSku: loadBalancerSku - networkPlugin: networkPlugin - networkPolicy: networkPolicy - } -} - -var hasAgentPool = !empty(agentPoolConfig) || !empty(agentPoolType) -var agentPoolSpec = hasAgentPool && !empty(agentPoolConfig) ? agentPoolConfig : empty(agentPoolType) ? {} : nodePoolPresets[agentPoolType] - -// Create additional user agent pool when specified -module agentPool 'aks-agent-pool.bicep' = if (hasAgentPool) { - name: 'aks-node-pool' - params: { - clusterName: managedCluster.outputs.clusterName - name: 'npuserpool' - config: union({ name: 'npuser', mode: 'User' }, nodePoolBase, agentPoolSpec) - } -} - -// Creates container registry (ACR) -module containerRegistry 'container-registry.bicep' = { - name: 'container-registry' - params: { - name: containerRegistryName - location: location - tags: tags - workspaceId: !empty(logAnalyticsName) ? logAnalytics.id : '' - } -} - -// Grant ACR Pull access from cluster managed identity to container registry -module containerRegistryAccess '../security/registry-access.bicep' = { - name: 'cluster-container-registry-access' - params: { - containerRegistryName: containerRegistry.outputs.name - principalId: managedCluster.outputs.clusterIdentity.objectId - } -} - -// Give AKS cluster access to the specified principal -module clusterAccess '../security/aks-managed-cluster-access.bicep' = if (!empty(principalId) && (enableAzureRbac || disableLocalAccounts)) { - name: 'cluster-access' - params: { - clusterName: managedCluster.outputs.clusterName - principalId: principalId - principalType: principalType - } -} - -// Give the AKS Cluster access to KeyVault -module clusterKeyVaultAccess '../security/keyvault-access.bicep' = { - name: 'cluster-keyvault-access' - params: { - keyVaultName: keyVaultName - principalId: managedCluster.outputs.clusterIdentity.objectId - } -} - -// Helpers for node pool configuration -var nodePoolBase = { - osType: 'Linux' - maxPods: 30 - type: 'VirtualMachineScaleSets' - upgradeSettings: { - maxSurge: '33%' - } -} - -var nodePoolPresets = { - CostOptimised: { - vmSize: 'Standard_B4ms' - count: 1 - minCount: 1 - maxCount: 3 - enableAutoScaling: true - availabilityZones: [] - } - Standard: { - vmSize: 'Standard_DS2_v2' - count: 3 - minCount: 3 - maxCount: 5 - enableAutoScaling: true - availabilityZones: [ - '1' - '2' - '3' - ] - } - HighSpec: { - vmSize: 'Standard_D4s_v3' - count: 3 - minCount: 3 - maxCount: 5 - enableAutoScaling: true - availabilityZones: [ - '1' - '2' - '3' - ] - } -} - -// Module outputs -@description('The resource name of the AKS cluster') -output clusterName string = managedCluster.outputs.clusterName - -@description('The AKS cluster identity') -output clusterIdentity object = managedCluster.outputs.clusterIdentity - -@description('The resource name of the ACR') -output containerRegistryName string = containerRegistry.outputs.name - -@description('The login server for the container registry') -output containerRegistryLoginServer string = containerRegistry.outputs.loginServer diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/appservice-appsettings.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/appservice-appsettings.bicep deleted file mode 100644 index f4b22f8..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/appservice-appsettings.bicep +++ /dev/null @@ -1,17 +0,0 @@ -metadata description = 'Updates app settings for an Azure App Service.' -@description('The name of the app service resource within the current resource group scope') -param name string - -@description('The app settings to be applied to the app service') -@secure() -param appSettings object - -resource appService 'Microsoft.Web/sites@2022-03-01' existing = { - name: name -} - -resource settings 'Microsoft.Web/sites/config@2022-03-01' = { - name: 'appsettings' - parent: appService - properties: appSettings -} diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/appservice.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/appservice.bicep deleted file mode 100644 index 06c35a5..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/appservice.bicep +++ /dev/null @@ -1,74 +0,0 @@ -metadata description = 'Creates an Azure App Service in an existing Azure App Service plan.' -param name string -param location string = resourceGroup().location -param tags object = {} - -// Reference Properties -param appServicePlanId string -param keyVaultName string = '' -param managedIdentity bool = !empty(keyVaultName) - -param storageEndpoint string = '' -param cosmosDbEndpoint string = '' -// Runtime Properties -@allowed([ - 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' -]) -param runtimeName string -param runtimeVersion string -param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' - -// Microsoft.Web/sites/config -param userAssignedIdentityId string = '' -param userAssignedIdentityClientId string = '' - -resource appService 'Microsoft.Web/sites@2022-03-01' = { - name: name - location: location - tags: tags - properties: { - serverFarmId: appServicePlanId - siteConfig: { - windowsFxVersion: runtimeNameAndVersion - webSocketsEnabled: true - } - httpsOnly: true - } - - identity: { - type: 'UserAssigned' - userAssignedIdentities: { - '${userAssignedIdentityId}': {} - } - } - - resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = { - name: 'ftp' - properties: { - allow: false - } - } - - resource basicPublishingCredentialsPoliciesScm 'basicPublishingCredentialsPolicies' = { - name: 'scm' - properties: { - allow: false - } - } -} - -// add connectionstring -resource symbolicname 'Microsoft.Web/sites/config@2022-09-01' = { - name: 'appsettings' - kind: 'string' - parent: appService - properties: { - AZURE_COSMOS_DB_NOSQL_ENDPOINT: cosmosDbEndpoint - STORAGE_URL: storageEndpoint - AZURE_CLIENT_ID: userAssignedIdentityClientId - } -} - -output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' -output name string = appService.name -output uri string = 'https://${appService.properties.defaultHostName}' diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/appserviceplan.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/appserviceplan.bicep deleted file mode 100644 index f2947b7..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/appserviceplan.bicep +++ /dev/null @@ -1,19 +0,0 @@ -metadata description = 'Creates an Azure App Service plan.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param kind string = '' -param reserved bool = true -param sku object - -resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { - name: name - location: location - tags: tags - sku: sku - kind: kind -} - -output id string = appServicePlan.id -output name string = appServicePlan.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-app-upsert.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-app-upsert.bicep deleted file mode 100644 index 870e74a..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-app-upsert.bicep +++ /dev/null @@ -1,109 +0,0 @@ -metadata description = 'Creates or updates an existing Azure Container App.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('The environment name for the container apps') -param containerAppsEnvironmentName string - -@description('The number of CPU cores allocated to a single container instance, e.g., 0.5') -param containerCpuCoreCount string = '0.5' - -@description('The maximum number of replicas to run. Must be at least 1.') -@minValue(1) -param containerMaxReplicas int = 10 - -@description('The amount of memory allocated to a single container instance, e.g., 1Gi') -param containerMemory string = '1.0Gi' - -@description('The minimum number of replicas to run. Must be at least 0.') -param containerMinReplicas int = 0 - -@description('The name of the container') -param containerName string = 'main' - -@description('The name of the container registry') -param containerRegistryName string = '' - -@description('Hostname suffix for container registry. Set when deploying to sovereign clouds') -param containerRegistryHostSuffix string = 'azurecr.io' - -@allowed([ 'http', 'grpc' ]) -@description('The protocol used by Dapr to connect to the app, e.g., HTTP or gRPC') -param daprAppProtocol string = 'http' - -@description('Enable or disable Dapr for the container app') -param daprEnabled bool = false - -@description('The Dapr app ID') -param daprAppId string = containerName - -@description('Specifies if the resource already exists') -param exists bool = false - -@description('Specifies if Ingress is enabled for the container app') -param ingressEnabled bool = true - -@description('The type of identity for the resource') -@allowed([ 'None', 'SystemAssigned', 'UserAssigned' ]) -param identityType string = 'None' - -@description('The name of the user-assigned identity') -param identityName string = '' - -@description('The name of the container image') -param imageName string = '' - -@description('The secrets required for the container') -@secure() -param secrets object = {} - -@description('The environment variables for the container') -param env array = [] - -@description('Specifies if the resource ingress is exposed externally') -param external bool = true - -@description('The service binds associated with the container') -param serviceBinds array = [] - -@description('The target port for the container') -param targetPort int = 80 - -resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = if (exists) { - name: name -} - -module app 'container-app.bicep' = { - name: '${deployment().name}-update' - params: { - name: name - location: location - tags: tags - identityType: identityType - identityName: identityName - ingressEnabled: ingressEnabled - containerName: containerName - containerAppsEnvironmentName: containerAppsEnvironmentName - containerRegistryName: containerRegistryName - containerRegistryHostSuffix: containerRegistryHostSuffix - containerCpuCoreCount: containerCpuCoreCount - containerMemory: containerMemory - containerMinReplicas: containerMinReplicas - containerMaxReplicas: containerMaxReplicas - daprEnabled: daprEnabled - daprAppId: daprAppId - daprAppProtocol: daprAppProtocol - secrets: secrets - external: external - env: env - imageName: !empty(imageName) ? imageName : exists ? existingApp.properties.template.containers[0].image : '' - targetPort: targetPort - serviceBinds: serviceBinds - } -} - -output defaultDomain string = app.outputs.defaultDomain -output imageName string = app.outputs.imageName -output name string = app.outputs.name -output uri string = app.outputs.uri diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-app.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-app.bicep deleted file mode 100644 index f1d9284..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-app.bicep +++ /dev/null @@ -1,169 +0,0 @@ -metadata description = 'Creates a container app in an Azure Container App environment.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('Allowed origins') -param allowedOrigins array = [] - -@description('Name of the environment for container apps') -param containerAppsEnvironmentName string - -@description('CPU cores allocated to a single container instance, e.g., 0.5') -param containerCpuCoreCount string = '0.5' - -@description('The maximum number of replicas to run. Must be at least 1.') -@minValue(1) -param containerMaxReplicas int = 10 - -@description('Memory allocated to a single container instance, e.g., 1Gi') -param containerMemory string = '1.0Gi' - -@description('The minimum number of replicas to run. Must be at least 0.') -param containerMinReplicas int = 0 - -@description('The name of the container') -param containerName string = 'main' - -@description('The name of the container registry') -param containerRegistryName string = '' - -@description('Hostname suffix for container registry. Set when deploying to sovereign clouds') -param containerRegistryHostSuffix string = 'azurecr.io' - -@description('The protocol used by Dapr to connect to the app, e.g., http or grpc') -@allowed([ 'http', 'grpc' ]) -param daprAppProtocol string = 'http' - -@description('The Dapr app ID') -param daprAppId string = containerName - -@description('Enable Dapr') -param daprEnabled bool = false - -@description('The environment variables for the container') -param env array = [] - -@description('Specifies if the resource ingress is exposed externally') -param external bool = true - -@description('The name of the user-assigned identity') -param identityName string = '' - -@description('The type of identity for the resource') -@allowed([ 'None', 'SystemAssigned', 'UserAssigned' ]) -param identityType string = 'None' - -@description('The name of the container image') -param imageName string = '' - -@description('Specifies if Ingress is enabled for the container app') -param ingressEnabled bool = true - -param revisionMode string = 'Single' - -@description('The secrets required for the container') -@secure() -param secrets object = {} - -@description('The service binds associated with the container') -param serviceBinds array = [] - -@description('The name of the container apps add-on to use. e.g. redis') -param serviceType string = '' - -@description('The target port for the container') -param targetPort int = 80 - -resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (!empty(identityName)) { - name: identityName -} - -// Private registry support requires both an ACR name and a User Assigned managed identity -var usePrivateRegistry = !empty(identityName) && !empty(containerRegistryName) - -// Automatically set to `UserAssigned` when an `identityName` has been set -var normalizedIdentityType = !empty(identityName) ? 'UserAssigned' : identityType - -module containerRegistryAccess '../security/registry-access.bicep' = if (usePrivateRegistry) { - name: '${deployment().name}-registry-access' - params: { - containerRegistryName: containerRegistryName - principalId: usePrivateRegistry ? userIdentity.properties.principalId : '' - } -} - -resource app 'Microsoft.App/containerApps@2025-01-01' = { - name: name - location: location - tags: tags - // It is critical that the identity is granted ACR pull access before the app is created - // otherwise the container app will throw a provision error - // This also forces us to use an user assigned managed identity since there would no way to - // provide the system assigned identity with the ACR pull access before the app is created - dependsOn: usePrivateRegistry ? [ containerRegistryAccess ] : [] - identity: { - type: normalizedIdentityType - userAssignedIdentities: !empty(identityName) && normalizedIdentityType == 'UserAssigned' ? { '${userIdentity.id}': {} } : null - } - properties: { - managedEnvironmentId: containerAppsEnvironment.id - configuration: { - activeRevisionsMode: revisionMode - ingress: ingressEnabled ? { - external: external - targetPort: targetPort - transport: 'auto' - corsPolicy: { - allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) - } - } : null - dapr: daprEnabled ? { - enabled: true - appId: daprAppId - appProtocol: daprAppProtocol - appPort: ingressEnabled ? targetPort : 0 - } : { enabled: false } - secrets: [for secret in items(secrets): { - name: secret.key - value: secret.value - }] - service: !empty(serviceType) ? { type: serviceType } : null - registries: usePrivateRegistry ? [ - { - server: '${containerRegistryName}.${containerRegistryHostSuffix}' - identity: userIdentity.id - } - ] : [] - } - template: { - serviceBinds: !empty(serviceBinds) ? serviceBinds : null - containers: [ - { - image: !empty(imageName) ? imageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - name: containerName - env: env - resources: { - cpu: json(containerCpuCoreCount) - memory: containerMemory - } - } - ] - scale: { - minReplicas: containerMinReplicas - maxReplicas: containerMaxReplicas - } - } - } -} - -resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' existing = { - name: containerAppsEnvironmentName -} - -output defaultDomain string = containerAppsEnvironment.properties.defaultDomain -output identityPrincipalId string = normalizedIdentityType == 'None' ? '' : (empty(identityName) ? app.identity.principalId : userIdentity.properties.principalId) -output imageName string = imageName -output name string = app.name -output serviceBind object = !empty(serviceType) ? { serviceId: app.id, name: name } : {} -output uri string = ingressEnabled ? 'https://${app.properties.configuration.ingress.fqdn}' : '' diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps-environment.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps-environment.bicep deleted file mode 100644 index d5fc9a6..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps-environment.bicep +++ /dev/null @@ -1,54 +0,0 @@ -metadata description = 'Creates an Azure Container Apps environment.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('Name of the Application Insights resource') -param applicationInsightsName string = '' - -@description('Specifies if Dapr is enabled') -param daprEnabled bool = false - -@description('Name of the Log Analytics workspace') -param logAnalyticsWorkspaceName string = '' - -@description('Subnet resource ID for the Container Apps environment') -param subnetResourceId string = '' - -@description('Whether to use an internal or external load balancer') -@allowed(['Internal', 'External']) -param loadBalancerType string = 'External' - -resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = { - name: name - location: location - tags: tags - properties: { - // appLogsConfiguration: { - // destination: 'log-analytics' - // logAnalyticsConfiguration: { - // customerId: logAnalyticsWorkspace.properties.customerId - // sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey - // } - // } - // daprAIInstrumentationKey: daprEnabled && !empty(applicationInsightsName) ? applicationInsights.properties.InstrumentationKey : '' - - // Add vnet configuration if a subnet is provided - vnetConfiguration: !empty(subnetResourceId) ? { - infrastructureSubnetId: subnetResourceId - internal: loadBalancerType == 'Internal' - } : null - } -} - -// resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { -// name: logAnalyticsWorkspaceName -// } - -// resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (daprEnabled && !empty(applicationInsightsName)) { -// name: applicationInsightsName -// } - -output defaultDomain string = containerAppsEnvironment.properties.defaultDomain -output id string = containerAppsEnvironment.id -output name string = containerAppsEnvironment.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps.bicep deleted file mode 100644 index a7143b0..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps.bicep +++ /dev/null @@ -1,52 +0,0 @@ -metadata description = 'Creates an Azure Container Registry and an Azure Container Apps environment.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param containerAppsEnvironmentName string -param containerRegistryName string -param containerRegistryResourceGroupName string = '' -param containerRegistryAdminUserEnabled bool = false -param logAnalyticsWorkspaceName string = '' -param applicationInsightsName string = '' -param daprEnabled bool = false - -// Virtual network and subnet parameters -param subnetResourceId string = '' -param loadBalancerType string = 'External' - -module containerAppsEnvironment 'container-apps-environment.bicep' = { - name: '${name}-container-apps-environment' - params: { - name: containerAppsEnvironmentName - location: location - tags: tags - logAnalyticsWorkspaceName: logAnalyticsWorkspaceName - applicationInsightsName: applicationInsightsName - daprEnabled: daprEnabled - subnetResourceId: subnetResourceId - loadBalancerType: loadBalancerType - } -} - -module containerRegistry 'container-registry.bicep' = { - name: '${name}-container-registry' - scope: !empty(containerRegistryResourceGroupName) ? resourceGroup(containerRegistryResourceGroupName) : resourceGroup() - params: { - name: containerRegistryName - location: location - adminUserEnabled: containerRegistryAdminUserEnabled - tags: tags - sku: { - name: 'Standard' - } - anonymousPullEnabled: false - } -} - -output defaultDomain string = containerAppsEnvironment.outputs.defaultDomain -output environmentName string = containerAppsEnvironment.outputs.name -output environmentId string = containerAppsEnvironment.outputs.id - -output registryLoginServer string = containerRegistry.outputs.loginServer -output registryName string = containerRegistry.outputs.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps/app.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps/app.bicep deleted file mode 100644 index 8658d07..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps/app.bicep +++ /dev/null @@ -1,123 +0,0 @@ -metadata description = 'Creates an Azure Container Apps app.' - -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('Name of the parent environment for the app.') -param parentEnvironmentName string - -@description('Specifies the docker container image to deploy.') -param containerImage string = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - -@description('Specifies the container port.') -param targetPort int = 80 - -@description('Number of CPU cores the container can use. Can have a maximum of two decimals.') -param cpuCores string = '0.25' - -@description('Amount of memory (in gibibytes, GiB) allocated to the container up to 4GiB. Can have a maximum of two decimals. Ratio with CPU cores must be equal to 2.') -param memorySize string = '0.5Gi' - -@description('Minimum number of replicas that will be deployed.') -@minValue(1) -@maxValue(25) -param minReplicas int = 1 - -@description('Maximum number of replicas that will be deployed.') -@minValue(1) -param maxReplicas int = 1 - -type envVar = { - name: string - secretRef: string? - value: string? -} - -@description('The environment variables for the container.') -param environmentVariables envVar[] = [] - -type secret = { - name: string - identity: string? - keyVaultUrl: string? - value: string? -} - -@description('The secrets required for the container') -param secrets secret[] = [] - -@description('Specifies if the resource ingress is exposed externally.') -param externalAccess bool = true - -@description('Specifies if Ingress is enabled for the container app.') -param ingressEnabled bool = true - -@description('Allowed CORS origins.') -param allowedOrigins string[] = [] - -type registry = { - server: string - identity: string? - username: string? - passwordSecretRef: string? -} - -@description('List of registries. Defaults to an empty list.') -param registries registry[] = [] - -@description('Enable system-assigned managed identity. Defaults to false.') -param enableSystemAssignedManagedIdentity bool = false - -@description('List of user-assigned managed identities. Defaults to an empty array.') -param userAssignedManagedIdentityIds string[] = [] - -resource environment 'Microsoft.App/managedEnvironments@2023-05-01' existing = { - name: parentEnvironmentName -} - -resource app 'Microsoft.App/containerApps@2023-05-01' = { - name: name - location: location - tags: tags - identity: { - type: enableSystemAssignedManagedIdentity ? !empty(userAssignedManagedIdentityIds) ? 'SystemAssigned,UserAssigned' : 'SystemAssigned' : !empty(userAssignedManagedIdentityIds) ? 'UserAssigned' : 'None' - userAssignedIdentities: !empty(userAssignedManagedIdentityIds) ? toObject(userAssignedManagedIdentityIds, uaid => uaid, uaid => {}) : null - } - properties: { - environmentId: environment.id - configuration: { - ingress: ingressEnabled ? { - external: externalAccess - targetPort: targetPort - transport: 'auto' - corsPolicy: { - allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) - } - } : null - secrets: secrets - registries: !empty(registries) ? registries : null - } - template: { - containers: [ - { - image: containerImage - name: name - resources: { - cpu: json(cpuCores) - memory: memorySize - } - env: environmentVariables - } - ] - scale: { - minReplicas: minReplicas - maxReplicas: maxReplicas - } - } - } -} - -output endpoint string = ingressEnabled ? 'https://${app.properties.configuration.ingress.fqdn}' : '' -output name string = app.name -output systemAssignedManagedIdentityPrincipalId string = enableSystemAssignedManagedIdentity ? app.identity.principalId : '' diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps/managed.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps/managed.bicep deleted file mode 100644 index d997160..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-apps/managed.bicep +++ /dev/null @@ -1,14 +0,0 @@ -metadata description = 'Creates an Azure Container Apps managed environment.' - -param name string -param location string = resourceGroup().location -param tags object = {} - -resource environment 'Microsoft.App/managedEnvironments@2023-05-01' = { - name: name - location: location - tags: tags - properties: {} -} - -output name string = environment.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-registry.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-registry.bicep deleted file mode 100644 index d14731c..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/container-registry.bicep +++ /dev/null @@ -1,137 +0,0 @@ -metadata description = 'Creates an Azure Container Registry.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('Indicates whether admin user is enabled') -param adminUserEnabled bool = false - -@description('Indicates whether anonymous pull is enabled') -param anonymousPullEnabled bool = false - -@description('Azure ad authentication as arm policy settings') -param azureADAuthenticationAsArmPolicy object = { - status: 'enabled' -} - -@description('Indicates whether data endpoint is enabled') -param dataEndpointEnabled bool = false - -@description('Encryption settings') -param encryption object = { - status: 'disabled' -} - -@description('Export policy settings') -param exportPolicy object = { - status: 'enabled' -} - -@description('Metadata search settings') -param metadataSearch string = 'Disabled' - -@description('Options for bypassing network rules') -param networkRuleBypassOptions string = 'AzureServices' - -@description('Public network access setting') -param publicNetworkAccess string = 'Enabled' - -@description('Quarantine policy settings') -param quarantinePolicy object = { - status: 'disabled' -} - -@description('Retention policy settings') -param retentionPolicy object = { - days: 7 - status: 'disabled' -} - -@description('Scope maps setting') -param scopeMaps array = [] - -@description('SKU settings') -param sku object = { - name: 'Basic' -} - -@description('Soft delete policy settings') -param softDeletePolicy object = { - retentionDays: 7 - status: 'disabled' -} - -@description('Trust policy settings') -param trustPolicy object = { - type: 'Notary' - status: 'disabled' -} - -@description('Zone redundancy setting') -param zoneRedundancy string = 'Disabled' - -@description('The log analytics workspace ID used for logging and monitoring') -param workspaceId string = '' - -// 2023-11-01-preview needed for metadataSearch -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = { - name: name - location: location - tags: tags - sku: sku - properties: { - adminUserEnabled: adminUserEnabled - anonymousPullEnabled: anonymousPullEnabled - dataEndpointEnabled: dataEndpointEnabled - encryption: encryption - metadataSearch: metadataSearch - networkRuleBypassOptions: networkRuleBypassOptions - policies:{ - quarantinePolicy: quarantinePolicy - trustPolicy: trustPolicy - retentionPolicy: retentionPolicy - exportPolicy: exportPolicy - azureADAuthenticationAsArmPolicy: azureADAuthenticationAsArmPolicy - softDeletePolicy: softDeletePolicy - } - publicNetworkAccess: publicNetworkAccess - zoneRedundancy: zoneRedundancy - } - - resource scopeMap 'scopeMaps' = [for scopeMap in scopeMaps: { - name: scopeMap.name - properties: scopeMap.properties - }] -} - -// TODO: Update diagnostics to be its own module -// Blocking issue: https://github.com/Azure/bicep/issues/622 -// Unable to pass in a `resource` scope or unable to use string interpolation in resource types -resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { - name: 'registry-diagnostics' - scope: containerRegistry - properties: { - workspaceId: workspaceId - logs: [ - { - category: 'ContainerRegistryRepositoryEvents' - enabled: true - } - { - category: 'ContainerRegistryLoginEvents' - enabled: true - } - ] - metrics: [ - { - category: 'AllMetrics' - enabled: true - timeGrain: 'PT1M' - } - ] - } -} - -output id string = containerRegistry.id -output loginServer string = containerRegistry.properties.loginServer -output name string = containerRegistry.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/functions.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/functions.bicep deleted file mode 100644 index cdf11e9..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/functions.bicep +++ /dev/null @@ -1,100 +0,0 @@ -metadata description = 'Creates an Azure Function in an existing Azure App Service plan.' -param name string -param location string = resourceGroup().location -param tags object = {} - -// Reference Properties -param applicationInsightsName string = '' -param appServicePlanId string -param keyVaultName string = '' -param managedIdentity bool = !empty(keyVaultName) || storageManagedIdentity -param storageAccountName string -param storageManagedIdentity bool = false -param virtualNetworkSubnetId string = '' - -// Runtime Properties -@allowed([ - 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' -]) -param runtimeName string -param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' -param runtimeVersion string - -// Function Settings -@allowed([ - '~4', '~3', '~2', '~1' -]) -param extensionVersion string = '~4' - -// Microsoft.Web/sites Properties -param kind string = 'functionapp,linux' - -// Microsoft.Web/sites/config -param allowedOrigins array = [] -param alwaysOn bool = true -param appCommandLine string = '' -@secure() -param appSettings object = {} -param clientAffinityEnabled bool = false -param enableOryxBuild bool = contains(kind, 'linux') -param functionAppScaleLimit int = -1 -param linuxFxVersion string = runtimeNameAndVersion -param minimumElasticInstanceCount int = -1 -param numberOfWorkers int = -1 -param scmDoBuildDuringDeployment bool = true -param use32BitWorkerProcess bool = false -param healthCheckPath string = '' - -module functions 'appservice.bicep' = { - name: '${name}-functions' - params: { - name: name - location: location - tags: tags - allowedOrigins: allowedOrigins - alwaysOn: alwaysOn - appCommandLine: appCommandLine - applicationInsightsName: applicationInsightsName - appServicePlanId: appServicePlanId - appSettings: union(appSettings, { - AzureWebJobsStorage__accountName: storageManagedIdentity ? storage.name : null - AzureWebJobsStorage: storageManagedIdentity ? null : 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' - FUNCTIONS_EXTENSION_VERSION: extensionVersion - FUNCTIONS_WORKER_RUNTIME: runtimeName - }) - clientAffinityEnabled: clientAffinityEnabled - enableOryxBuild: enableOryxBuild - functionAppScaleLimit: functionAppScaleLimit - healthCheckPath: healthCheckPath - keyVaultName: keyVaultName - kind: kind - linuxFxVersion: linuxFxVersion - managedIdentity: managedIdentity - minimumElasticInstanceCount: minimumElasticInstanceCount - numberOfWorkers: numberOfWorkers - runtimeName: runtimeName - runtimeVersion: runtimeVersion - runtimeNameAndVersion: runtimeNameAndVersion - scmDoBuildDuringDeployment: scmDoBuildDuringDeployment - use32BitWorkerProcess: use32BitWorkerProcess - virtualNetworkSubnetId: virtualNetworkSubnetId - } -} - -module storageOwnerRole '../../core/security/role.bicep' = if (storageManagedIdentity) { - name: 'search-index-contrib-role-api' - params: { - principalId: functions.outputs.identityPrincipalId - // Search Index Data Contributor - roleDefinitionId: '8ebe5a00-799e-43f5-93ac-243d3dce84a7' - principalType: 'ServicePrincipal' - } -} - -resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { - name: storageAccountName -} - -output identityPrincipalId string = managedIdentity ? functions.outputs.identityPrincipalId : '' -output name string = functions.outputs.name -output uri string = functions.outputs.uri diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/host/staticwebapp.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/host/staticwebapp.bicep deleted file mode 100644 index cedaf90..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/host/staticwebapp.bicep +++ /dev/null @@ -1,22 +0,0 @@ -metadata description = 'Creates an Azure Static Web Apps instance.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object = { - name: 'Free' - tier: 'Free' -} - -resource web 'Microsoft.Web/staticSites@2022-03-01' = { - name: name - location: location - tags: tags - sku: sku - properties: { - provider: 'Custom' - } -} - -output name string = web.name -output uri string = 'https://${web.properties.defaultHostname}' diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/monitor/applicationinsights-dashboard.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/monitor/applicationinsights-dashboard.bicep deleted file mode 100644 index d082e66..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/monitor/applicationinsights-dashboard.bicep +++ /dev/null @@ -1,1236 +0,0 @@ -metadata description = 'Creates a dashboard for an Application Insights instance.' -param name string -param applicationInsightsName string -param location string = resourceGroup().location -param tags object = {} - -// 2020-09-01-preview because that is the latest valid version -resource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { - name: name - location: location - tags: tags - properties: { - lenses: [ - { - order: 0 - parts: [ - { - position: { - x: 0 - y: 0 - colSpan: 2 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'id' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/AspNetOverviewPinnedPart' - asset: { - idInputName: 'id' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'overview' - } - } - { - position: { - x: 2 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/ProactiveDetectionAsyncPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'ProactiveDetection' - } - } - { - position: { - x: 3 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'ResourceId' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 4 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - endTime: null - createdTime: '2018-05-04T01:20:33.345Z' - isInitialTime: true - grain: 1 - useDashboardTimeRange: false - } - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 5 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - endTime: null - createdTime: '2018-05-08T18:47:35.237Z' - isInitialTime: true - grain: 1 - useDashboardTimeRange: false - } - } - { - name: 'ConfigurationId' - value: '78ce933e-e864-4b05-a27b-71fd55a6afad' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/AppMapButtonPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 0 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Usage' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 3 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - endTime: null - createdTime: '2018-05-04T01:22:35.782Z' - isInitialTime: true - grain: 1 - useDashboardTimeRange: false - } - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/UsageUsersOverviewPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 4 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Reliability' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 7 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ResourceId' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - { - name: 'DataModel' - value: { - version: '1.0.0' - timeContext: { - durationMs: 86400000 - createdTime: '2018-05-04T23:42:40.072Z' - isInitialTime: false - grain: 1 - useDashboardTimeRange: false - } - } - isOptional: true - } - { - name: 'ConfigurationId' - value: '8a02f7bf-ac0f-40e1-afe9-f0e72cfee77f' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/CuratedBladeFailuresPinnedPart' - isAdapter: true - asset: { - idInputName: 'ResourceId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'failures' - } - } - { - position: { - x: 8 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Responsiveness\r\n' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 11 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ResourceId' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - { - name: 'DataModel' - value: { - version: '1.0.0' - timeContext: { - durationMs: 86400000 - createdTime: '2018-05-04T23:43:37.804Z' - isInitialTime: false - grain: 1 - useDashboardTimeRange: false - } - } - isOptional: true - } - { - name: 'ConfigurationId' - value: '2a8ede4f-2bee-4b9c-aed9-2db0e8a01865' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/CuratedBladePerformancePinnedPart' - isAdapter: true - asset: { - idInputName: 'ResourceId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'performance' - } - } - { - position: { - x: 12 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Browser' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 15 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'MetricsExplorerJsonDefinitionId' - value: 'BrowserPerformanceTimelineMetrics' - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - createdTime: '2018-05-08T12:16:27.534Z' - isInitialTime: false - grain: 1 - useDashboardTimeRange: false - } - } - { - name: 'CurrentFilter' - value: { - eventTypes: [ - 4 - 1 - 3 - 5 - 2 - 6 - 13 - ] - typeFacets: {} - isPermissive: false - } - } - { - name: 'id' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/MetricsExplorerBladePinnedPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'browser' - } - } - { - position: { - x: 0 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'sessions/count' - aggregationType: 5 - namespace: 'microsoft.insights/components/kusto' - metricVisualization: { - displayName: 'Sessions' - color: '#47BDF5' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'users/count' - aggregationType: 5 - namespace: 'microsoft.insights/components/kusto' - metricVisualization: { - displayName: 'Users' - color: '#7E58FF' - } - } - ] - title: 'Unique sessions and users' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'segmentationUsers' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 4 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'requests/failed' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Failed requests' - color: '#EC008C' - } - } - ] - title: 'Failed requests' - visualization: { - chartType: 3 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'failures' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 8 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'requests/duration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Server response time' - color: '#00BCF2' - } - } - ] - title: 'Server response time' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'performance' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 12 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/networkDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Page load network connect time' - color: '#7E58FF' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/processingDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Client processing time' - color: '#44F1C8' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/sendDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Send request time' - color: '#EB9371' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/receiveDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Receiving response time' - color: '#0672F1' - } - } - ] - title: 'Average page load time breakdown' - visualization: { - chartType: 3 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 0 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'availabilityResults/availabilityPercentage' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Availability' - color: '#47BDF5' - } - } - ] - title: 'Average availability' - visualization: { - chartType: 3 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'availability' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 4 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'exceptions/server' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Server exceptions' - color: '#47BDF5' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'dependencies/failed' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Dependency failures' - color: '#7E58FF' - } - } - ] - title: 'Server exceptions and Dependency failures' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 8 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/processorCpuPercentage' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Processor time' - color: '#47BDF5' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/processCpuPercentage' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Process CPU' - color: '#7E58FF' - } - } - ] - title: 'Average processor and process CPU utilization' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 12 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'exceptions/browser' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Browser exceptions' - color: '#47BDF5' - } - } - ] - title: 'Browser exceptions' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 0 - y: 8 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'availabilityResults/count' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Availability test results count' - color: '#47BDF5' - } - } - ] - title: 'Availability test results count' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 4 - y: 8 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/processIOBytesPerSecond' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Process IO rate' - color: '#47BDF5' - } - } - ] - title: 'Average process I/O rate' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 8 - y: 8 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/memoryAvailableBytes' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Available memory' - color: '#47BDF5' - } - } - ] - title: 'Average available memory' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - ] - } - ] - } -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { - name: applicationInsightsName -} diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/monitor/applicationinsights.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/monitor/applicationinsights.bicep deleted file mode 100644 index 850e9fe..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/monitor/applicationinsights.bicep +++ /dev/null @@ -1,31 +0,0 @@ -metadata description = 'Creates an Application Insights instance based on an existing Log Analytics workspace.' -param name string -param dashboardName string = '' -param location string = resourceGroup().location -param tags object = {} -param logAnalyticsWorkspaceId string - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { - name: name - location: location - tags: tags - kind: 'web' - properties: { - Application_Type: 'web' - WorkspaceResourceId: logAnalyticsWorkspaceId - } -} - -module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (!empty(dashboardName)) { - name: 'application-insights-dashboard' - params: { - name: dashboardName - location: location - applicationInsightsName: applicationInsights.name - } -} - -output connectionString string = applicationInsights.properties.ConnectionString -output id string = applicationInsights.id -output instrumentationKey string = applicationInsights.properties.InstrumentationKey -output name string = applicationInsights.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/monitor/loganalytics.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/monitor/loganalytics.bicep deleted file mode 100644 index 33f9dc2..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/monitor/loganalytics.bicep +++ /dev/null @@ -1,22 +0,0 @@ -metadata description = 'Creates a Log Analytics workspace.' -param name string -param location string = resourceGroup().location -param tags object = {} - -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { - name: name - location: location - tags: tags - properties: any({ - retentionInDays: 30 - features: { - searchVersion: 1 - } - sku: { - name: 'PerGB2018' - } - }) -} - -output id string = logAnalytics.id -output name string = logAnalytics.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/monitor/monitoring.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/monitor/monitoring.bicep deleted file mode 100644 index 7476125..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/monitor/monitoring.bicep +++ /dev/null @@ -1,33 +0,0 @@ -metadata description = 'Creates an Application Insights instance and a Log Analytics workspace.' -param logAnalyticsName string -param applicationInsightsName string -param applicationInsightsDashboardName string = '' -param location string = resourceGroup().location -param tags object = {} - -module logAnalytics 'loganalytics.bicep' = { - name: 'loganalytics' - params: { - name: logAnalyticsName - location: location - tags: tags - } -} - -module applicationInsights 'applicationinsights.bicep' = { - name: 'applicationinsights' - params: { - name: applicationInsightsName - location: location - tags: tags - dashboardName: applicationInsightsDashboardName - logAnalyticsWorkspaceId: logAnalytics.outputs.id - } -} - -output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString -output applicationInsightsId string = applicationInsights.outputs.id -output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey -output applicationInsightsName string = applicationInsights.outputs.name -output logAnalyticsWorkspaceId string = logAnalytics.outputs.id -output logAnalyticsWorkspaceName string = logAnalytics.outputs.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/networking/cdn-endpoint.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/networking/cdn-endpoint.bicep deleted file mode 100644 index 5e8ab69..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/networking/cdn-endpoint.bicep +++ /dev/null @@ -1,52 +0,0 @@ -metadata description = 'Adds an endpoint to an Azure CDN profile.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('The name of the CDN profile resource') -@minLength(1) -param cdnProfileName string - -@description('Delivery policy rules') -param deliveryPolicyRules array = [] - -@description('The origin URL for the endpoint') -@minLength(1) -param originUrl string - -resource endpoint 'Microsoft.Cdn/profiles/endpoints@2022-05-01-preview' = { - parent: cdnProfile - name: name - location: location - tags: tags - properties: { - originHostHeader: originUrl - isHttpAllowed: false - isHttpsAllowed: true - queryStringCachingBehavior: 'UseQueryString' - optimizationType: 'GeneralWebDelivery' - origins: [ - { - name: replace(originUrl, '.', '-') - properties: { - hostName: originUrl - originHostHeader: originUrl - priority: 1 - weight: 1000 - enabled: true - } - } - ] - deliveryPolicy: { - rules: deliveryPolicyRules - } - } -} - -resource cdnProfile 'Microsoft.Cdn/profiles@2022-05-01-preview' existing = { - name: cdnProfileName -} - -output id string = endpoint.id -output name string = endpoint.name -output uri string = 'https://${endpoint.properties.hostName}' diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/networking/cdn-profile.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/networking/cdn-profile.bicep deleted file mode 100644 index 27669ee..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/networking/cdn-profile.bicep +++ /dev/null @@ -1,34 +0,0 @@ -metadata description = 'Creates an Azure CDN profile.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('The pricing tier of this CDN profile') -@allowed([ - 'Custom_Verizon' - 'Premium_AzureFrontDoor' - 'Premium_Verizon' - 'StandardPlus_955BandWidth_ChinaCdn' - 'StandardPlus_AvgBandWidth_ChinaCdn' - 'StandardPlus_ChinaCdn' - 'Standard_955BandWidth_ChinaCdn' - 'Standard_Akamai' - 'Standard_AvgBandWidth_ChinaCdn' - 'Standard_AzureFrontDoor' - 'Standard_ChinaCdn' - 'Standard_Microsoft' - 'Standard_Verizon' -]) -param sku string = 'Standard_Microsoft' - -resource profile 'Microsoft.Cdn/profiles@2022-05-01-preview' = { - name: name - location: location - tags: tags - sku: { - name: sku - } -} - -output id string = profile.id -output name string = profile.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/networking/cdn.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/networking/cdn.bicep deleted file mode 100644 index de98a1f..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/networking/cdn.bicep +++ /dev/null @@ -1,42 +0,0 @@ -metadata description = 'Creates an Azure CDN profile with a single endpoint.' -param location string = resourceGroup().location -param tags object = {} - -@description('Name of the CDN endpoint resource') -param cdnEndpointName string - -@description('Name of the CDN profile resource') -param cdnProfileName string - -@description('Delivery policy rules') -param deliveryPolicyRules array = [] - -@description('Origin URL for the CDN endpoint') -param originUrl string - -module cdnProfile 'cdn-profile.bicep' = { - name: 'cdn-profile' - params: { - name: cdnProfileName - location: location - tags: tags - } -} - -module cdnEndpoint 'cdn-endpoint.bicep' = { - name: 'cdn-endpoint' - params: { - name: cdnEndpointName - location: location - tags: tags - cdnProfileName: cdnProfile.outputs.name - originUrl: originUrl - deliveryPolicyRules: deliveryPolicyRules - } -} - -output endpointName string = cdnEndpoint.outputs.name -output endpointId string = cdnEndpoint.outputs.id -output profileName string = cdnProfile.outputs.name -output profileId string = cdnProfile.outputs.id -output uri string = cdnEndpoint.outputs.uri diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/networking/vnet.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/networking/vnet.bicep deleted file mode 100644 index e82865d..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/networking/vnet.bicep +++ /dev/null @@ -1,52 +0,0 @@ -// filepath: /Users/nickgreenfield1/workspace/Durable-Task-Scheduler/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/networking/vnet.bicep -@description('The name of the Virtual Network') -param name string - -@description('The Azure region where the Virtual Network should exist') -param location string = resourceGroup().location - -@description('Optional tags for the resources') -param tags object = {} - -@description('The address prefixes of the Virtual Network') -param addressPrefixes array = ['10.0.0.0/16'] - -@description('The subnets to create in the Virtual Network') -param subnets array = [ - { - name: 'infrastructure-subnet' - properties: { - addressPrefix: '10.0.0.0/21' - // Container Apps environments don't need pre-configured delegations - they handle this themselves - delegations: [] - privateEndpointNetworkPolicies: 'Disabled' - privateLinkServiceNetworkPolicies: 'Enabled' - } - } - { - name: 'workload-subnet' - properties: { - addressPrefix: '10.0.8.0/21' - delegations: [] - privateEndpointNetworkPolicies: 'Disabled' - privateLinkServiceNetworkPolicies: 'Enabled' - } - } -] - -resource vnet 'Microsoft.Network/virtualNetworks@2022-07-01' = { - name: name - location: location - tags: tags - properties: { - addressSpace: { - addressPrefixes: addressPrefixes - } - subnets: subnets - } -} - -output id string = vnet.id -output name string = vnet.name -output infrastructureSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', name, 'infrastructure-subnet') -output workloadSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', name, 'workload-subnet') diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/search/search-services.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/search/search-services.bicep deleted file mode 100644 index 33fd83e..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/search/search-services.bicep +++ /dev/null @@ -1,68 +0,0 @@ -metadata description = 'Creates an Azure AI Search instance.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object = { - name: 'standard' -} - -param authOptions object = {} -param disableLocalAuth bool = false -param disabledDataExfiltrationOptions array = [] -param encryptionWithCmk object = { - enforcement: 'Unspecified' -} -@allowed([ - 'default' - 'highDensity' -]) -param hostingMode string = 'default' -param networkRuleSet object = { - bypass: 'None' - ipRules: [] -} -param partitionCount int = 1 -@allowed([ - 'enabled' - 'disabled' -]) -param publicNetworkAccess string = 'enabled' -param replicaCount int = 1 -@allowed([ - 'disabled' - 'free' - 'standard' -]) -param semanticSearch string = 'disabled' - -var searchIdentityProvider = (sku.name == 'free') ? null : { - type: 'SystemAssigned' -} - -resource search 'Microsoft.Search/searchServices@2021-04-01-preview' = { - name: name - location: location - tags: tags - // The free tier does not support managed identity - identity: searchIdentityProvider - properties: { - authOptions: disableLocalAuth ? null : authOptions - disableLocalAuth: disableLocalAuth - disabledDataExfiltrationOptions: disabledDataExfiltrationOptions - encryptionWithCmk: encryptionWithCmk - hostingMode: hostingMode - networkRuleSet: networkRuleSet - partitionCount: partitionCount - publicNetworkAccess: publicNetworkAccess - replicaCount: replicaCount - semanticSearch: semanticSearch - } - sku: sku -} - -output id string = search.id -output endpoint string = 'https://${name}.search.windows.net/' -output name string = search.name -output principalId string = !empty(searchIdentityProvider) ? search.identity.principalId : '' - diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/security/aks-managed-cluster-access.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/security/aks-managed-cluster-access.bicep deleted file mode 100644 index aedb080..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/security/aks-managed-cluster-access.bicep +++ /dev/null @@ -1,27 +0,0 @@ -metadata description = 'Assigns RBAC role to the specified AKS cluster and principal.' - -@description('The AKS cluster name used as the target of the role assignments.') -param clusterName string - -@description('The principal ID to assign the role to.') -param principalId string - -@description('The principal type to assign the role to.') -@allowed(['Device','ForeignGroup','Group','ServicePrincipal','User']) -param principalType string = 'User' - -var aksClusterAdminRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b1ff04bb-8a4e-4dc4-8eb5-8693973ce19b') - -resource aksRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - scope: aksCluster // Use when specifying a scope that is different than the deployment scope - name: guid(subscription().id, resourceGroup().id, principalId, aksClusterAdminRole) - properties: { - roleDefinitionId: aksClusterAdminRole - principalType: principalType - principalId: principalId - } -} - -resource aksCluster 'Microsoft.ContainerService/managedClusters@2023-10-02-preview' existing = { - name: clusterName -} diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/security/configstore-access.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/security/configstore-access.bicep deleted file mode 100644 index de72b94..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/security/configstore-access.bicep +++ /dev/null @@ -1,21 +0,0 @@ -@description('Name of Azure App Configuration store') -param configStoreName string - -@description('The principal ID of the service principal to assign the role to') -param principalId string - -resource configStore 'Microsoft.AppConfiguration/configurationStores@2023-03-01' existing = { - name: configStoreName -} - -var configStoreDataReaderRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '516239f1-63e1-4d78-a4de-a74fb236a071') - -resource configStoreDataReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(subscription().id, resourceGroup().id, principalId, configStoreDataReaderRole) - scope: configStore - properties: { - roleDefinitionId: configStoreDataReaderRole - principalId: principalId - principalType: 'ServicePrincipal' - } -} diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/security/keyvault-access.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/security/keyvault-access.bicep deleted file mode 100644 index 316775f..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/security/keyvault-access.bicep +++ /dev/null @@ -1,22 +0,0 @@ -metadata description = 'Assigns an Azure Key Vault access policy.' -param name string = 'add' - -param keyVaultName string -param permissions object = { secrets: [ 'get', 'list' ] } -param principalId string - -resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { - parent: keyVault - name: name - properties: { - accessPolicies: [ { - objectId: principalId - tenantId: subscription().tenantId - permissions: permissions - } ] - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/security/keyvault-secret.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/security/keyvault-secret.bicep deleted file mode 100644 index 7441b29..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/security/keyvault-secret.bicep +++ /dev/null @@ -1,31 +0,0 @@ -metadata description = 'Creates or updates a secret in an Azure Key Vault.' -param name string -param tags object = {} -param keyVaultName string -param contentType string = 'string' -@description('The value of the secret. Provide only derived values like blob storage access, but do not hard code any secrets in your templates') -@secure() -param secretValue string - -param enabled bool = true -param exp int = 0 -param nbf int = 0 - -resource keyVaultSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - name: name - tags: tags - parent: keyVault - properties: { - attributes: { - enabled: enabled - exp: exp - nbf: nbf - } - contentType: contentType - value: secretValue - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/security/keyvault.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/security/keyvault.bicep deleted file mode 100644 index c449137..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/security/keyvault.bicep +++ /dev/null @@ -1,34 +0,0 @@ -metadata description = 'Creates an Azure Key Vault.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param principalId string = '' - -@description('Allow the key vault to be used during resource creation.') -param enabledForDeployment bool = false -@description('Allow the key vault to be used for template deployment.') -param enabledForTemplateDeployment bool = false - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { - name: name - location: location - tags: tags - properties: { - tenantId: subscription().tenantId - sku: { family: 'A', name: 'standard' } - accessPolicies: !empty(principalId) ? [ - { - objectId: principalId - permissions: { secrets: [ 'get', 'list' ] } - tenantId: subscription().tenantId - } - ] : [] - enabledForDeployment: enabledForDeployment - enabledForTemplateDeployment: enabledForTemplateDeployment - } -} - -output endpoint string = keyVault.properties.vaultUri -output id string = keyVault.id -output name string = keyVault.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/security/registry-access.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/security/registry-access.bicep deleted file mode 100644 index fc66837..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/security/registry-access.bicep +++ /dev/null @@ -1,19 +0,0 @@ -metadata description = 'Assigns ACR Pull permissions to access an Azure Container Registry.' -param containerRegistryName string -param principalId string - -var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - -resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - scope: containerRegistry // Use when specifying a scope that is different than the deployment scope - name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole) - properties: { - roleDefinitionId: acrPullRole - principalType: 'ServicePrincipal' - principalId: principalId - } -} - -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { - name: containerRegistryName -} diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/security/role.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/security/role.bicep deleted file mode 100644 index 0b30cfd..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/security/role.bicep +++ /dev/null @@ -1,21 +0,0 @@ -metadata description = 'Creates a role assignment for a service principal.' -param principalId string - -@allowed([ - 'Device' - 'ForeignGroup' - 'Group' - 'ServicePrincipal' - 'User' -]) -param principalType string = 'ServicePrincipal' -param roleDefinitionId string - -resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId) - properties: { - principalId: principalId - principalType: principalType - roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) - } -} diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/storage/storage-account.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/storage/storage-account.bicep deleted file mode 100644 index 8586cf5..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/storage/storage-account.bicep +++ /dev/null @@ -1,102 +0,0 @@ -metadata description = 'Creates an Azure storage account.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@allowed([ - 'Cool' - 'Hot' - 'Premium' ]) -param accessTier string = 'Hot' -param allowBlobPublicAccess bool = false -param allowCrossTenantReplication bool = true -param allowSharedKeyAccess bool = true -param containers array = [] -param corsRules array = [] -param defaultToOAuthAuthentication bool = false -param deleteRetentionPolicy object = {} -@allowed([ 'AzureDnsZone', 'Standard' ]) -param dnsEndpointType string = 'Standard' -param files array = [] -param kind string = 'StorageV2' -param minimumTlsVersion string = 'TLS1_2' -param queues array = [] -param shareDeleteRetentionPolicy object = {} -param supportsHttpsTrafficOnly bool = true -param tables array = [] -param networkAcls object = { - bypass: 'AzureServices' - defaultAction: 'Allow' -} -@allowed([ 'Enabled', 'Disabled' ]) -param publicNetworkAccess string = 'Enabled' -param sku object = { name: 'Standard_LRS' } - -resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = { - name: name - location: location - tags: tags - kind: kind - sku: sku - properties: { - accessTier: accessTier - allowBlobPublicAccess: allowBlobPublicAccess - allowCrossTenantReplication: allowCrossTenantReplication - allowSharedKeyAccess: allowSharedKeyAccess - defaultToOAuthAuthentication: defaultToOAuthAuthentication - dnsEndpointType: dnsEndpointType - minimumTlsVersion: minimumTlsVersion - networkAcls: networkAcls - publicNetworkAccess: publicNetworkAccess - supportsHttpsTrafficOnly: supportsHttpsTrafficOnly - } - - resource blobServices 'blobServices' = if (!empty(containers)) { - name: 'default' - properties: { - cors: { - corsRules: corsRules - } - deleteRetentionPolicy: deleteRetentionPolicy - } - resource container 'containers' = [for container in containers: { - name: container.name - properties: { - publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None' - } - }] - } - - resource fileServices 'fileServices' = if (!empty(files)) { - name: 'default' - properties: { - cors: { - corsRules: corsRules - } - shareDeleteRetentionPolicy: shareDeleteRetentionPolicy - } - } - - resource queueServices 'queueServices' = if (!empty(queues)) { - name: 'default' - properties: { - - } - resource queue 'queues' = [for queue in queues: { - name: queue.name - properties: { - metadata: {} - } - }] - } - - resource tableServices 'tableServices' = if (!empty(tables)) { - name: 'default' - properties: {} - } -} - -output id string = storage.id -output name string = storage.name -output primaryEndpoints object = storage.properties.primaryEndpoints -output blobEndpoint string = storage.properties.primaryEndpoints.blob diff --git a/samples/durable-task-sdks/java/function-chaining/infra/core/testing/loadtesting.bicep b/samples/durable-task-sdks/java/function-chaining/infra/core/testing/loadtesting.bicep deleted file mode 100644 index 4678108..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/core/testing/loadtesting.bicep +++ /dev/null @@ -1,15 +0,0 @@ -param name string -param location string = resourceGroup().location -param managedIdentity bool = false -param tags object = {} - -resource loadTest 'Microsoft.LoadTestService/loadTests@2022-12-01' = { - name: name - location: location - tags: tags - identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } - properties: { - } -} - -output loadTestingName string = loadTest.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/main.bicep b/samples/durable-task-sdks/java/function-chaining/infra/main.bicep deleted file mode 100644 index 10fe1c1..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/main.bicep +++ /dev/null @@ -1,174 +0,0 @@ -targetScope = 'subscription' - -// The main bicep module to provision Azure resources. -// For a more complete walkthrough to understand how this file works with azd, -// see https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-create - -@minLength(1) -@maxLength(64) -@description('Name of the the environment which is used to generate a short unique hash used in all resources.') -param environmentName string - -@minLength(1) -@description('Primary location for all resources') -param location string - -@description('Id of the user or app to assign application roles') -param principalId string = '' - -param containerAppsEnvName string = '' -param containerAppsAppName string = '' -param containerRegistryName string = '' -param dtsLocation string = 'centralus' -param dtsSkuName string = 'Dedicated' -param dtsCapacity int = 1 -param dtsName string = '' -param taskHubName string = '' - -param sampleAppServiceName string = 'sampleapp' - -// Optional parameters to override the default azd resource naming conventions. -// Add the following to main.parameters.json to provide values: -// "resourceGroupName": { -// "value": "myGroupName" -// } -param resourceGroupName string = '' - -var abbrs = loadJsonContent('./abbreviations.json') - -// tags that should be applied to all resources. -var tags = { - // Tag all resources with the environment name. - 'azd-env-name': environmentName -} - -// Generate a unique token to be used in naming resources. -// Remove linter suppression after using. -#disable-next-line no-unused-vars -var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) - -// Organize resources in a resource group -resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { - name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' - location: location - tags: tags -} - -// Add resources to be provisioned below. -// A full example that leverages azd bicep modules can be seen in the todo-python-mongo template: -// https://github.com/Azure-Samples/todo-python-mongo/tree/main/infra - -// Create a user assigned identity -module identity './app/user-assigned-identity.bicep' = { - name: 'identity' - scope: rg - params: { - name: 'dts-ca-identity' - } -} - -module identityAssignDTS './core/security/role.bicep' = { - name: 'identityAssignDTS' - scope: rg - params: { - principalId: identity.outputs.principalId - roleDefinitionId: '0ad04412-c4d5-4796-b79c-f76d14c8d402' - principalType: 'ServicePrincipal' - } -} - -module identityAssignDTSDash './core/security/role.bicep' = { - name: 'identityAssignDTSDash' - scope: rg - params: { - principalId: principalId - roleDefinitionId: '0ad04412-c4d5-4796-b79c-f76d14c8d402' - principalType: 'User' - } -} - -// Create virtual network with subnets for Container Apps -module vnet './core/networking/vnet.bicep' = { - name: 'vnet' - scope: rg - params: { - name: '${abbrs.networkVirtualNetworks}${resourceToken}' - location: location - tags: tags - } -} - -// Container apps env and registry -module containerAppsEnv './core/host/container-apps.bicep' = { - name: 'container-apps' - scope: rg - params: { - name: 'app' - containerAppsEnvironmentName: !empty(containerAppsEnvName) ? containerAppsEnvName : '${abbrs.appManagedEnvironments}${resourceToken}' - containerRegistryName: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' - location: location - // Add subnet configuration - subnetResourceId: vnet.outputs.infrastructureSubnetId - loadBalancerType: 'External' // Can be changed to 'Internal' if needed - } -} - -module dts './app/dts.bicep' = { - scope: rg - name: 'dtsResource' - params: { - name: !empty(dtsName) ? dtsName : '${abbrs.dts}${resourceToken}' - taskhubname: !empty(taskHubName) ? taskHubName : '${abbrs.taskhub}${resourceToken}' - location: dtsLocation - tags: tags - ipAllowlist: [ - '0.0.0.0/0' - ] - skuName: dtsSkuName - skuCapacity: dtsCapacity - } -} - - -// Container app -module sampleApp 'app/app.bicep' = { - name: sampleAppServiceName - scope: rg - params: { - appName: !empty(containerAppsAppName) ? '${containerAppsAppName}-sampleapp' : '${abbrs.appContainerApps}${resourceToken}-sampleapp' - containerAppsEnvironmentName: containerAppsEnv.outputs.environmentName - containerRegistryName: containerAppsEnv.outputs.registryName - userAssignedManagedIdentity: { - resourceId: identity.outputs.resourceId - clientId: identity.outputs.clientId - } - location: location - tags: tags - serviceName: 'sampleapp' - exists: false - identityName: identity.outputs.name - dtsEndpoint: dts.outputs.dts_URL - taskHubName: dts.outputs.TASKHUB_NAME - } -} - -// Add outputs from the deployment here, if needed. -// -// This allows the outputs to be referenced by other bicep deployments in the deployment pipeline, -// or by the local machine as a way to reference created resources in Azure for local development. -// Secrets should not be added here. -// -// Outputs are automatically saved in the local azd environment .env file. -// To see these outputs, run `azd env get-values`, or `azd env get-values --output json` for json output. -output AZURE_LOCATION string = location -output AZURE_TENANT_ID string = tenant().tenantId -// Container outputs -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerAppsEnv.outputs.registryLoginServer -output AZURE_CONTAINER_REGISTRY_NAME string = containerAppsEnv.outputs.registryName - -// // Application outputs -// output AZURE_CONTAINER_APP_ENDPOINT string = web.outputs.endpoint -// output AZURE_CONTAINER_ENVIRONMENT_NAME string = web.outputs.envName - -// Identity outputs -output AZURE_USER_ASSIGNED_IDENTITY_NAME string = identity.outputs.name diff --git a/samples/durable-task-sdks/java/function-chaining/infra/main.parameters.json b/samples/durable-task-sdks/java/function-chaining/infra/main.parameters.json deleted file mode 100644 index c8d3453..0000000 --- a/samples/durable-task-sdks/java/function-chaining/infra/main.parameters.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "environmentName": { - "value": "${AZURE_ENV_NAME}" - }, - "location": { - "value": "${AZURE_LOCATION}" - }, - "principalId": { - "value": "${AZURE_PRINCIPAL_ID}" - } - } -} diff --git a/samples/durable-task-sdks/python/entities/Dockerfile.client b/samples/durable-task-sdks/python/entities/Dockerfile.client new file mode 100644 index 0000000..8e7b363 --- /dev/null +++ b/samples/durable-task-sdks/python/entities/Dockerfile.client @@ -0,0 +1,13 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Copy requirements first to leverage Docker cache +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy client application code +COPY client.py . + +# Run the client application +CMD ["python", "client.py"] diff --git a/samples/durable-task-sdks/python/entities/Dockerfile.worker b/samples/durable-task-sdks/python/entities/Dockerfile.worker new file mode 100644 index 0000000..31c99a2 --- /dev/null +++ b/samples/durable-task-sdks/python/entities/Dockerfile.worker @@ -0,0 +1,13 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Copy requirements first to leverage Docker cache +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy worker application code +COPY worker.py . + +# Run the worker application +CMD ["python", "worker.py"] diff --git a/samples/durable-task-sdks/python/entities/README.md b/samples/durable-task-sdks/python/entities/README.md new file mode 100644 index 0000000..ad36d34 --- /dev/null +++ b/samples/durable-task-sdks/python/entities/README.md @@ -0,0 +1,273 @@ +# Durable Entities Pattern + +## Description of the Sample + +This sample demonstrates the Durable Entities pattern with the Azure Durable Task Scheduler using the Python SDK. Durable entities are stateful objects that maintain state across operations and can be accessed by orchestrations or directly by clients. + +In this sample: +1. A counter entity is defined that supports `add`, `subtract`, `get`, and `reset` operations +2. The client signals the entity directly to modify its state +3. Orchestrations interact with entities using `signal_entity` and `call_entity` +4. Entity state is automatically persisted and survives restarts + +This pattern is useful for: +- Building aggregators and accumulators +- Maintaining shared state across workflows +- Implementing distributed counters, caches, or locks +- Creating actor-like programming models + +## Prerequisites + +1. [Python 3.9+](https://www.python.org/downloads/) +2. [Docker](https://www.docker.com/products/docker-desktop/) (for running the emulator) installed +3. [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli) (if using a deployed Durable Task Scheduler) + +## Configuring Durable Task Scheduler + +There are two ways to run this sample locally: + +### Using the Emulator (Recommended) + +The emulator simulates a scheduler and taskhub in a Docker container, making it ideal for development and learning. + +1. Pull the Docker Image for the Emulator: + ```bash + docker pull mcr.microsoft.com/dts/dts-emulator:latest + ``` + +1. Run the Emulator: + ```bash + docker run --name dtsemulator -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest + ``` +Wait a few seconds for the container to be ready. + +Note: The example code automatically uses the default emulator settings (endpoint: http://localhost:8080, taskhub: default). You don't need to set any environment variables. + +### Using a Deployed Scheduler and Taskhub in Azure + +Local development with a deployed scheduler: + +1. Install the durable task scheduler CLI extension: + + ```bash + az upgrade + az extension add --name durabletask --allow-preview true + ``` + +1. Create a resource group in a region where the Durable Task Scheduler is available: + + ```bash + az provider show --namespace Microsoft.DurableTask --query "resourceTypes[?resourceType=='schedulers'].locations | [0]" --out table + ``` + + ```bash + az group create --name my-resource-group --location + ``` +1. Create a durable task scheduler resource: + + ```bash + az durabletask scheduler create \ + --resource-group my-resource-group \ + --name my-scheduler \ + --ip-allowlist '["0.0.0.0/0"]' \ + --sku-name "Dedicated" \ + --sku-capacity 1 \ + --tags "{'myattribute':'myvalue'}" + ``` + +1. Create a task hub within the scheduler resource: + + ```bash + az durabletask taskhub create \ + --resource-group my-resource-group \ + --scheduler-name my-scheduler \ + --name "my-taskhub" + ``` + +1. Grant the current user permission to connect to the `my-taskhub` task hub: + + ```bash + subscriptionId=$(az account show --query "id" -o tsv) + loggedInUser=$(az account show --query "user.name" -o tsv) + + az role assignment create \ + --assignee $loggedInUser \ + --role "Durable Task Data Contributor" \ + --scope "/subscriptions/$subscriptionId/resourceGroups/my-resource-group/providers/Microsoft.DurableTask/schedulers/my-scheduler/taskHubs/my-taskhub" + ``` + +## How to Run the Sample + +Once you have set up either the emulator or deployed scheduler, follow these steps to run the sample: + +1. First, activate your Python virtual environment (if you're using one): + ```bash + python -m venv venv + source venv/bin/activate # On Windows, use: venv\Scripts\activate + ``` + +1. If you're using a deployed scheduler, you need set Environment Variables: + ```bash + export ENDPOINT=$(az durabletask scheduler show \ + --resource-group my-resource-group \ + --name my-scheduler \ + --query "properties.endpoint" \ + --output tsv) + + export TASKHUB="my-taskhub" + ``` + +1. Install the required packages: + ```bash + pip install -r requirements.txt + ``` + +1. Start the worker in a terminal: + ```bash + python worker.py + ``` + You should see output indicating the worker has started and registered the entity and orchestration. + +1. In a new terminal (with the virtual environment activated if applicable), run the client: + > **Note:** Remember to set the environment variables again if you're using a deployed scheduler. + + ```bash + python client.py [entity-key] + ``` + You can optionally provide an entity key as an argument. If not provided, "my-counter" will be used. + +## Understanding Durable Entities + +### Entity Definition + +Entities are defined as functions that receive an `EntityContext`: + +```python +def counter(ctx: entities.EntityContext, input: int): + state = ctx.get_state(int, 0) # Get current state with default + + if ctx.operation == "add": + state += input + ctx.set_state(state) + elif ctx.operation == "get": + return state +``` + +### Entity Operations + +Entities support two types of operations: + +1. **Signal (fire-and-forget)**: Sends a message to the entity without waiting for a response + ```python + ctx.signal_entity(entity_id=entity_id, operation_name="add", input=10) + ``` + +2. **Call (request-response)**: Sends a message and waits for the result + ```python + value = yield ctx.call_entity(entity=entity_id, operation="get") + ``` + +### Client Operations + +Entities can also be signaled directly from clients: +```python +entity_id = entities.EntityInstanceId("counter", "my-counter") +client.signal_entity(entity_id, "add", input=100) +``` + +## Deploying with Azure Developer CLI (AZD) + +This sample includes an `azure.yaml` configuration file that allows you to deploy the entire solution to Azure using Azure Developer CLI (AZD). + +> **Note:** This sample uses the shared infrastructure templates located at [`samples/infra/`](../../../infra/). + +### Prerequisites for AZD Deployment + +1. Install [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) +2. Authenticate with Azure: + ```bash + azd auth login + ``` + +### Deployment Steps + +1. Navigate to the Entities sample directory: + ```bash + cd /path/to/Durable-Task-Scheduler/samples/durable-task-sdks/python/entities + ``` + +2. Initialize the Azure Developer CLI project (only needed the first time): + ```bash + azd init + ``` + This step prepares the environment for deployment and creates necessary configuration files. + +3. Provision resources and deploy the application: + ```bash + azd up + ``` + This command will: + - Provision Azure resources (including Azure Container Apps and Durable Task Scheduler) + - Build and deploy both the Client and Worker components + - Set up the necessary connections between components + +3. After deployment completes, AZD will display URLs for your deployed services. + +4. Monitor your entities and orchestrations using the Azure Portal by navigating to your Durable Task Scheduler resource. + +5. To confirm the sample is working correctly, view the application logs through the Azure Portal: + - Navigate to the Azure Portal (https://portal.azure.com) + - Go to your resource group where the application was deployed + - Find and select the Container Apps for both the worker and client components + - For each Container App: + - Click on "Log stream" in the left navigation menu under "Monitoring" + - View the real-time logs showing entity operations and orchestration results + +## Understanding the Output + +When you run the sample, you'll see output from both the worker and client processes: + +### Worker Output +The worker shows: +- Registration of the counter entity and orchestrator +- Log entries when entity operations are performed +- The state changes for each counter entity + +### Client Output +The client shows: +- Direct entity signals sent to the counter +- Orchestration scheduling and completion +- Final counter values returned from entity operations + +Example output: +``` +Starting entity operations demo - 5 orchestrations +=== Direct Entity Operations === +Signaling entity 'my-counter' to add 100 +Signaling entity 'my-counter' to subtract 25 +=== Orchestration-based Entity Operations === +Scheduling orchestration #1 for entity 'my-counter-orch-1' +Orchestration completed successfully with result: "Counter 'my-counter-orch-1' final value: 12" +``` + +## Reviewing the Orchestration in the Durable Task Scheduler Dashboard + +To access the Durable Task Scheduler Dashboard and review your entities: + +### Using the Emulator +1. Navigate to http://localhost:8082 in your web browser +2. Click on the "default" task hub +3. You'll see orchestration instances in the list +4. Click on an instance ID to view the execution details, which will show: + - Entity signals and calls + - Entity state changes + - The final result + +### Using a Deployed Scheduler +1. Navigate to the Scheduler resource in the Azure portal +2. Go to the Task Hub subresource that you're using +3. Click on the dashboard URL in the top right corner +4. Search for your orchestration instance ID +5. Review the execution details and entity interactions + +The dashboard helps visualize how entities maintain state across multiple operations and orchestrations. diff --git a/samples/durable-task-sdks/python/entities/azure.yaml b/samples/durable-task-sdks/python/entities/azure.yaml new file mode 100644 index 0000000..1fc2c2a --- /dev/null +++ b/samples/durable-task-sdks/python/entities/azure.yaml @@ -0,0 +1,27 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +# This is an example starter azure.yaml file containing several example services in comments below. +# Make changes as needed to describe your application setup. +# To learn more about the azure.yaml file, visit https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/azd-schema + +# Name of the application. +metadata: + template: entities-python +name: dts-entities +infra: + path: ../../../infra +services: + client: + project: . + language: python + host: containerapp + apiVersion: 2025-01-01 + docker: + path: ./Dockerfile.client + worker: + project: . + language: python + host: containerapp + apiVersion: 2025-01-01 + docker: + path: ./Dockerfile.worker diff --git a/samples/durable-task-sdks/python/entities/client.py b/samples/durable-task-sdks/python/entities/client.py new file mode 100644 index 0000000..979865f --- /dev/null +++ b/samples/durable-task-sdks/python/entities/client.py @@ -0,0 +1,157 @@ +import asyncio +import logging +import sys +import os +from azure.identity import DefaultAzureCredential, ManagedIdentityCredential +from azure.core.exceptions import ClientAuthenticationError +from durabletask import client as durable_client, entities +from durabletask.azuremanaged.client import DurableTaskSchedulerClient + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def main(): + """Main entry point for the client application.""" + logger.info("Starting Entities pattern client...") + + # Get environment variables for taskhub and endpoint with defaults + taskhub_name = os.getenv("TASKHUB", "default") + endpoint = os.getenv("ENDPOINT", "http://localhost:8080") + # Default interval between orchestrations (in seconds) + interval = int(os.getenv("ORCHESTRATION_INTERVAL", "60")) + + print(f"Using taskhub: {taskhub_name}") + print(f"Using endpoint: {endpoint}") + print(f"Orchestration interval: {interval} seconds") + + # Credential handling with better error management + credential = None + if endpoint != "http://localhost:8080": + try: + # Check if we're running in Azure with a managed identity + client_id = os.getenv("AZURE_MANAGED_IDENTITY_CLIENT_ID") + if client_id: + logger.info(f"Using Managed Identity with client ID: {client_id}") + credential = ManagedIdentityCredential(client_id=client_id) + # Test the credential to make sure it works + credential.get_token("https://management.azure.com/.default") + logger.info("Successfully authenticated with Managed Identity") + else: + # Fall back to DefaultAzureCredential only if no client ID is available + logger.info("No client ID found, falling back to DefaultAzureCredential") + credential = DefaultAzureCredential() + except Exception as e: + logger.error(f"Authentication error: {e}") + logger.warning("Continuing without authentication - this may only work with local emulator") + credential = None + + # Create a client using Azure Managed Durable Task + logger.info(f"Creating client with endpoint={endpoint}, credential={'provided' if credential else 'none'}") + client = DurableTaskSchedulerClient( + host_address=endpoint, + secure_channel=endpoint != "http://localhost:8080", + taskhub=taskhub_name, + token_credential=credential + ) + + # Get entity key from command line or use default + entity_key = sys.argv[1] if len(sys.argv) > 1 else "my-counter" + + # Set up orchestration parameters + TOTAL_ORCHESTRATIONS = 5 # Total number of orchestrations to run + INTERVAL_SECONDS = 5 # Time between orchestrations in seconds + completed_orchestrations = 0 + failed_orchestrations = 0 + + try: + logger.info(f"Starting entity operations demo - {TOTAL_ORCHESTRATIONS} orchestrations") + + # First, demonstrate direct entity signaling from client + logger.info("=== Direct Entity Operations ===") + entity_id = entities.EntityInstanceId("counter", entity_key) + + # Signal the entity directly from the client + logger.info(f"Signaling entity '{entity_key}' to add 100") + client.signal_entity(entity_id, "add", input=100) + + # Wait a moment for the signal to be processed + await asyncio.sleep(2) + + logger.info(f"Signaling entity '{entity_key}' to subtract 25") + client.signal_entity(entity_id, "subtract", input=25) + + await asyncio.sleep(2) + + # Now run orchestrations that interact with entities + logger.info("=== Orchestration-based Entity Operations ===") + + instance_ids = [] + for i in range(TOTAL_ORCHESTRATIONS): + try: + # Create a unique entity key for this orchestration + instance_entity_key = f"{entity_key}-orch-{i+1}" + logger.info(f"Scheduling orchestration #{i+1} for entity '{instance_entity_key}'") + + # Schedule the orchestration + instance_id = client.schedule_new_orchestration( + "counter_workflow", + input=instance_entity_key + ) + + instance_ids.append(instance_id) + logger.info(f"Orchestration #{i+1} scheduled with ID: {instance_id}") + + # Wait before scheduling next orchestration (except for the last one) + if i < TOTAL_ORCHESTRATIONS - 1: + logger.info(f"Waiting {INTERVAL_SECONDS} seconds before scheduling next orchestration...") + await asyncio.sleep(INTERVAL_SECONDS) + + except Exception as e: + logger.error(f"Error scheduling orchestration #{i+1}: {e}") + + logger.info(f"All {len(instance_ids)} orchestrations scheduled. Waiting for completion...") + + # Wait for all orchestrations to complete + for idx, instance_id in enumerate(instance_ids): + try: + logger.info(f"Waiting for orchestration {idx+1}/{len(instance_ids)} (ID: {instance_id})...") + result = client.wait_for_orchestration_completion( + instance_id, + timeout=120 + ) + + if result: + if result.runtime_status == durable_client.OrchestrationStatus.FAILED: + failed_orchestrations += 1 + logger.error(f"Orchestration {instance_id} failed") + elif result.runtime_status == durable_client.OrchestrationStatus.COMPLETED: + completed_orchestrations += 1 + logger.info(f"Orchestration {instance_id} completed successfully with result: {result.serialized_output}") + else: + logger.info(f"Orchestration {instance_id} status: {result.runtime_status}") + else: + logger.warning(f"Orchestration {instance_id} did not complete within the timeout period") + except Exception as e: + logger.error(f"Error waiting for orchestration {instance_id}: {e}") + + logger.info(f"All orchestrations processed. Successful: {completed_orchestrations}, Failed: {failed_orchestrations}") + + # Final summary + logger.info("=== Entity Demo Complete ===") + logger.info(f"Direct entity signals sent to '{entity_key}'") + logger.info(f"Orchestrations completed: {completed_orchestrations}/{TOTAL_ORCHESTRATIONS}") + + except KeyboardInterrupt: + logger.info("Client stopped by user") + + except Exception as e: + logger.exception(f"Unexpected error: {e}") + + finally: + logger.info("Client shutting down") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/samples/durable-task-sdks/python/entities/requirements.txt b/samples/durable-task-sdks/python/entities/requirements.txt new file mode 100644 index 0000000..1bc468d --- /dev/null +++ b/samples/durable-task-sdks/python/entities/requirements.txt @@ -0,0 +1,2 @@ +durabletask-azuremanaged +azure-identity diff --git a/samples/durable-task-sdks/python/entities/worker.py b/samples/durable-task-sdks/python/entities/worker.py new file mode 100644 index 0000000..3c36081 --- /dev/null +++ b/samples/durable-task-sdks/python/entities/worker.py @@ -0,0 +1,127 @@ +import asyncio +import logging +import os +from azure.identity import DefaultAzureCredential, ManagedIdentityCredential +from azure.core.exceptions import ClientAuthenticationError +from durabletask import task, entities +from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +# Function-based entity for a counter +def counter(ctx: entities.EntityContext, input: int): + """Function-based entity that maintains a counter state. + + Supports operations: add, subtract, get, reset + """ + state = ctx.get_state(int, 0) # Get state with default 0 + + if ctx.operation == "add": + state += input + ctx.set_state(state) + logger.info(f"Counter '{ctx.entity_id.key}': Added {input}, new value: {state}") + elif ctx.operation == "subtract": + state -= input + ctx.set_state(state) + logger.info(f"Counter '{ctx.entity_id.key}': Subtracted {input}, new value: {state}") + elif ctx.operation == "get": + logger.info(f"Counter '{ctx.entity_id.key}': Current value: {state}") + return state + elif ctx.operation == "reset": + ctx.set_state(0) + logger.info(f"Counter '{ctx.entity_id.key}': Reset to 0") + + +# Orchestrator that interacts with the counter entity +def counter_workflow(ctx: task.OrchestrationContext, entity_key: str): + """Orchestration that demonstrates entity interactions. + + This orchestration: + 1. Creates/accesses a counter entity + 2. Adds values to the counter + 3. Gets the current value + 4. Subtracts a value + 5. Returns the final count + """ + entity_id = entities.EntityInstanceId("counter", entity_key) + + # Signal entity operations (fire-and-forget) + ctx.signal_entity(entity_id=entity_id, operation_name="add", input=10) + ctx.signal_entity(entity_id=entity_id, operation_name="add", input=5) + ctx.signal_entity(entity_id=entity_id, operation_name="subtract", input=3) + + # Call entity and wait for result (note: call_entity uses 'entity' and 'operation' params) + value = yield ctx.call_entity(entity=entity_id, operation="get") + + return f"Counter '{entity_key}' final value: {value}" + + +# Activity to log entity state +def log_entity_state(ctx: task.ActivityContext, message: str) -> str: + """Activity function that logs messages.""" + logger.info(f"Entity state log: {message}") + return message + + +async def main(): + """Main entry point for the worker process.""" + logger.info("Starting Entities pattern worker...") + + # Get environment variables for taskhub and endpoint with defaults + taskhub_name = os.getenv("TASKHUB", "default") + endpoint = os.getenv("ENDPOINT", "http://localhost:8080") + + print(f"Using taskhub: {taskhub_name}") + print(f"Using endpoint: {endpoint}") + + # Credential handling with better error management + credential = None + if endpoint != "http://localhost:8080": + try: + # Check if we're running in Azure with a managed identity + client_id = os.getenv("AZURE_MANAGED_IDENTITY_CLIENT_ID") + if client_id: + logger.info(f"Using Managed Identity with client ID: {client_id}") + credential = ManagedIdentityCredential(client_id=client_id) + # Test the credential to make sure it works + credential.get_token("https://management.azure.com/.default") + logger.info("Successfully authenticated with Managed Identity") + else: + # Fall back to DefaultAzureCredential only if no client ID is available + logger.info("No client ID found, falling back to DefaultAzureCredential") + credential = DefaultAzureCredential() + except Exception as e: + logger.error(f"Authentication error: {e}") + logger.warning("Continuing without authentication - this may only work with local emulator") + credential = None + + with DurableTaskSchedulerWorker( + host_address=endpoint, + secure_channel=endpoint != "http://localhost:8080", + taskhub=taskhub_name, + token_credential=credential + ) as worker: + + # Register entities, activities and orchestrators + worker.add_entity(counter) + worker.add_activity(log_entity_state) + worker.add_orchestrator(counter_workflow) + + # Start the worker (without awaiting) + worker.start() + + try: + # Keep the worker running + while True: + await asyncio.sleep(1) + except KeyboardInterrupt: + logger.info("Worker shutdown initiated") + + logger.info("Worker stopped") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/samples/durable-task-sdks/python/function-chaining/Dockerfile.client b/samples/durable-task-sdks/python/function-chaining/Dockerfile.client index e3e0ef0..8e7b363 100644 --- a/samples/durable-task-sdks/python/function-chaining/Dockerfile.client +++ b/samples/durable-task-sdks/python/function-chaining/Dockerfile.client @@ -1,4 +1,4 @@ -FROM python:3.9-slim +FROM python:3.12-slim WORKDIR /app diff --git a/samples/durable-task-sdks/python/function-chaining/Dockerfile.worker b/samples/durable-task-sdks/python/function-chaining/Dockerfile.worker index 9165aca..31c99a2 100644 --- a/samples/durable-task-sdks/python/function-chaining/Dockerfile.worker +++ b/samples/durable-task-sdks/python/function-chaining/Dockerfile.worker @@ -1,4 +1,4 @@ -FROM python:3.9-slim +FROM python:3.12-slim WORKDIR /app diff --git a/samples/durable-task-sdks/python/function-chaining/README.md b/samples/durable-task-sdks/python/function-chaining/README.md index ce17df2..bed08ff 100644 --- a/samples/durable-task-sdks/python/function-chaining/README.md +++ b/samples/durable-task-sdks/python/function-chaining/README.md @@ -139,6 +139,8 @@ Once you have set up either the emulator or deployed scheduler, follow these ste This sample includes an `azure.yaml` configuration file that allows you to deploy the entire solution to Azure using Azure Developer CLI (AZD). +> **Note:** This sample uses the shared infrastructure templates located at [`samples/infra/`](../../../infra/). + ### Prerequisites for AZD Deployment 1. Install [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) diff --git a/samples/durable-task-sdks/python/function-chaining/azure.yaml b/samples/durable-task-sdks/python/function-chaining/azure.yaml index b13b841..ea8968f 100644 --- a/samples/durable-task-sdks/python/function-chaining/azure.yaml +++ b/samples/durable-task-sdks/python/function-chaining/azure.yaml @@ -8,6 +8,8 @@ metadata: template: hello-azd-python name: dts-quickstart +infra: + path: ../../../infra services: client: project: . diff --git a/samples/durable-task-sdks/python/function-chaining/infra/abbreviations.json b/samples/durable-task-sdks/python/function-chaining/infra/abbreviations.json deleted file mode 100644 index 1f9a112..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/abbreviations.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "analysisServicesServers": "as", - "apiManagementService": "apim-", - "appConfigurationStores": "appcs-", - "appManagedEnvironments": "cae-", - "appContainerApps": "ca-", - "authorizationPolicyDefinitions": "policy-", - "automationAutomationAccounts": "aa-", - "blueprintBlueprints": "bp-", - "blueprintBlueprintsArtifacts": "bpa-", - "cacheRedis": "redis-", - "cdnProfiles": "cdnp-", - "cdnProfilesEndpoints": "cdne-", - "cognitiveServicesAccounts": "cog-", - "cognitiveServicesFormRecognizer": "cog-fr-", - "cognitiveServicesTextAnalytics": "cog-ta-", - "cognitiveServicesSpeech": "cog-sp-", - "computeAvailabilitySets": "avail-", - "computeCloudServices": "cld-", - "computeDiskEncryptionSets": "des", - "computeDisks": "disk", - "computeDisksOs": "osdisk", - "computeGalleries": "gal", - "computeSnapshots": "snap-", - "computeVirtualMachines": "vm", - "computeVirtualMachineScaleSets": "vmss-", - "containerInstanceContainerGroups": "ci", - "containerRegistryRegistries": "cr", - "containerServiceManagedClusters": "aks-", - "databricksWorkspaces": "dbw-", - "dataFactoryFactories": "adf-", - "dataLakeAnalyticsAccounts": "dla", - "dataLakeStoreAccounts": "dls", - "dataMigrationServices": "dms-", - "dBforMySQLServers": "mysql-", - "dBforPostgreSQLServers": "psql-", - "devicesIotHubs": "iot-", - "devicesProvisioningServices": "provs-", - "devicesProvisioningServicesCertificates": "pcert-", - "documentDBDatabaseAccounts": "cosmos-", - "eventGridDomains": "evgd-", - "eventGridDomainsTopics": "evgt-", - "eventGridEventSubscriptions": "evgs-", - "eventHubNamespaces": "evhns-", - "eventHubNamespacesEventHubs": "evh-", - "hdInsightClustersHadoop": "hadoop-", - "hdInsightClustersHbase": "hbase-", - "hdInsightClustersKafka": "kafka-", - "hdInsightClustersMl": "mls-", - "hdInsightClustersSpark": "spark-", - "hdInsightClustersStorm": "storm-", - "hybridComputeMachines": "arcs-", - "insightsActionGroups": "ag-", - "insightsComponents": "appi-", - "keyVaultVaults": "kv-", - "kubernetesConnectedClusters": "arck", - "kustoClusters": "dec", - "kustoClustersDatabases": "dedb", - "loadTesting": "lt-", - "logicIntegrationAccounts": "ia-", - "logicWorkflows": "logic-", - "machineLearningServicesWorkspaces": "mlw-", - "managedIdentityUserAssignedIdentities": "id-", - "managementManagementGroups": "mg-", - "migrateAssessmentProjects": "migr-", - "networkApplicationGateways": "agw-", - "networkApplicationSecurityGroups": "asg-", - "networkAzureFirewalls": "afw-", - "networkBastionHosts": "bas-", - "networkConnections": "con-", - "networkDnsZones": "dnsz-", - "networkExpressRouteCircuits": "erc-", - "networkFirewallPolicies": "afwp-", - "networkFirewallPoliciesWebApplication": "waf", - "networkFirewallPoliciesRuleGroups": "wafrg", - "networkFrontDoors": "fd-", - "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", - "networkLoadBalancersExternal": "lbe-", - "networkLoadBalancersInternal": "lbi-", - "networkLoadBalancersInboundNatRules": "rule-", - "networkLocalNetworkGateways": "lgw-", - "networkNatGateways": "ng-", - "networkNetworkInterfaces": "nic-", - "networkNetworkSecurityGroups": "nsg-", - "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", - "networkNetworkWatchers": "nw-", - "networkPrivateDnsZones": "pdnsz-", - "networkPrivateLinkServices": "pl-", - "networkPublicIPAddresses": "pip-", - "networkPublicIPPrefixes": "ippre-", - "networkRouteFilters": "rf-", - "networkRouteTables": "rt-", - "networkRouteTablesRoutes": "udr-", - "networkTrafficManagerProfiles": "traf-", - "networkVirtualNetworkGateways": "vgw-", - "networkVirtualNetworks": "vnet-", - "networkVirtualNetworksSubnets": "snet-", - "networkVirtualNetworksVirtualNetworkPeerings": "peer-", - "networkVirtualWans": "vwan-", - "networkVpnGateways": "vpng-", - "networkVpnGatewaysVpnConnections": "vcn-", - "networkVpnGatewaysVpnSites": "vst-", - "notificationHubsNamespaces": "ntfns-", - "notificationHubsNamespacesNotificationHubs": "ntf-", - "operationalInsightsWorkspaces": "log-", - "portalDashboards": "dash-", - "powerBIDedicatedCapacities": "pbi-", - "purviewAccounts": "pview-", - "recoveryServicesVaults": "rsv-", - "resourcesResourceGroups": "rg-", - "searchSearchServices": "srch-", - "serviceBusNamespaces": "sb-", - "serviceBusNamespacesQueues": "sbq-", - "serviceBusNamespacesTopics": "sbt-", - "serviceEndPointPolicies": "se-", - "serviceFabricClusters": "sf-", - "signalRServiceSignalR": "sigr", - "sqlManagedInstances": "sqlmi-", - "sqlServers": "sql-", - "sqlServersDataWarehouse": "sqldw-", - "sqlServersDatabases": "sqldb-", - "sqlServersDatabasesStretch": "sqlstrdb-", - "storageStorageAccounts": "st", - "storageStorageAccountsVm": "stvm", - "storSimpleManagers": "ssimp", - "streamAnalyticsCluster": "asa-", - "synapseWorkspaces": "syn", - "synapseWorkspacesAnalyticsWorkspaces": "synw", - "synapseWorkspacesSqlPoolsDedicated": "syndp", - "synapseWorkspacesSqlPoolsSpark": "synsp", - "timeSeriesInsightsEnvironments": "tsi-", - "webServerFarms": "plan-", - "webSitesAppService": "app-", - "webSitesAppServiceEnvironment": "ase-", - "webSitesFunctions": "func-", - "webStaticSites": "stapp-", - "dts": "dts-", - "taskhub": "taskhub-" -} diff --git a/samples/durable-task-sdks/python/function-chaining/infra/app/app.bicep b/samples/durable-task-sdks/python/function-chaining/infra/app/app.bicep deleted file mode 100644 index c06c068..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/app/app.bicep +++ /dev/null @@ -1,54 +0,0 @@ -param appName string -param location string = resourceGroup().location -param tags object = {} - -param identityName string -param containerAppsEnvironmentName string -param containerRegistryName string -param serviceName string = 'aca' -param exists bool -param dtsEndpoint string -param taskHubName string - -type managedIdentity = { - resourceId: string - clientId: string -} - -@description('Unique identifier for user-assigned managed identity.') -param userAssignedManagedIdentity managedIdentity - -module containerAppsApp '../core/host/container-app.bicep' = { - name: 'container-apps-${serviceName}' - params: { - name: appName - containerAppsEnvironmentName: containerAppsEnvironmentName - containerRegistryName: containerRegistryName - location: location - tags: union(tags, { 'azd-service-name': serviceName }) - ingressEnabled: false - secrets: { - 'azure-managed-identity-client-id': userAssignedManagedIdentity.clientId - } - env: [ - { - name: 'AZURE_MANAGED_IDENTITY_CLIENT_ID' - secretRef: 'azure-managed-identity-client-id' - } - { - name: 'ENDPOINT' - value: dtsEndpoint - } - { - name: 'TASKHUB' - value: taskHubName - } - ] - identityName: identityName - containerMinReplicas: 0 - containerMaxReplicas: 10 - } -} - -output endpoint string = containerAppsApp.outputs.uri -output envName string = containerAppsApp.outputs.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/app/dts.bicep b/samples/durable-task-sdks/python/function-chaining/infra/app/dts.bicep deleted file mode 100644 index c435832..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/app/dts.bicep +++ /dev/null @@ -1,29 +0,0 @@ -param ipAllowlist array -param location string -param tags object = {} -param name string -param taskhubname string -param skuName string -param skuCapacity int - -resource dts 'Microsoft.DurableTask/schedulers@2024-10-01-preview' = { - location: location - tags: tags - name: name - properties: { - ipAllowlist: ipAllowlist - sku: { - name: skuName - capacity: skuCapacity - } - } -} - -resource taskhub 'Microsoft.DurableTask/schedulers/taskhubs@2024-10-01-preview' = { - parent: dts - name: taskhubname -} - -output dts_NAME string = dts.name -output dts_URL string = dts.properties.endpoint -output TASKHUB_NAME string = taskhub.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/app/user-assigned-identity.bicep b/samples/durable-task-sdks/python/function-chaining/infra/app/user-assigned-identity.bicep deleted file mode 100644 index 0583ab8..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/app/user-assigned-identity.bicep +++ /dev/null @@ -1,17 +0,0 @@ -metadata description = 'Creates a Microsoft Entra user-assigned identity.' - -param name string -param location string = resourceGroup().location -param tags object = {} - -resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { - name: name - location: location - tags: tags -} - -output name string = identity.name -output resourceId string = identity.id -output principalId string = identity.properties.principalId -output clientId string = identity.properties.clientId -output tenantId string = identity.properties.tenantId diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/ai/cognitiveservices.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/ai/cognitiveservices.bicep deleted file mode 100644 index 76778e6..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/ai/cognitiveservices.bicep +++ /dev/null @@ -1,56 +0,0 @@ -metadata description = 'Creates an Azure Cognitive Services instance.' -param name string -param location string = resourceGroup().location -param tags object = {} -@description('The custom subdomain name used to access the API. Defaults to the value of the name parameter.') -param customSubDomainName string = name -param disableLocalAuth bool = false -param deployments array = [] -param kind string = 'OpenAI' - -@allowed([ 'Enabled', 'Disabled' ]) -param publicNetworkAccess string = 'Enabled' -param sku object = { - name: 'S0' -} - -param allowedIpRules array = [] -param networkAcls object = empty(allowedIpRules) ? { - defaultAction: 'Allow' -} : { - ipRules: allowedIpRules - defaultAction: 'Deny' -} - -resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { - name: name - location: location - tags: tags - kind: kind - properties: { - customSubDomainName: customSubDomainName - publicNetworkAccess: publicNetworkAccess - networkAcls: networkAcls - disableLocalAuth: disableLocalAuth - } - sku: sku -} - -@batchSize(1) -resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in deployments: { - parent: account - name: deployment.name - properties: { - model: deployment.model - raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null - } - sku: contains(deployment, 'sku') ? deployment.sku : { - name: 'Standard' - capacity: 20 - } -}] - -output endpoint string = account.properties.endpoint -output endpoints object = account.properties.endpoints -output id string = account.id -output name string = account.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/ai/hub-dependencies.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/ai/hub-dependencies.bicep deleted file mode 100644 index eeabee7..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/ai/hub-dependencies.bicep +++ /dev/null @@ -1,170 +0,0 @@ -param location string = resourceGroup().location -param tags object = {} - -@description('Name of the key vault') -param keyVaultName string -@description('Name of the storage account') -param storageAccountName string -@description('Name of the OpenAI cognitive services') -param openAiName string -@description('Array of OpenAI model deployments') -param openAiModelDeployments array = [] -@description('Name of the Log Analytics workspace') -param logAnalyticsName string = '' -@description('Name of the Application Insights instance') -param applicationInsightsName string = '' -@description('Name of the container registry') -param containerRegistryName string = '' -@description('Name of the Azure Cognitive Search service') -param searchServiceName string = '' - -module keyVault '../security/keyvault.bicep' = { - name: 'keyvault' - params: { - location: location - tags: tags - name: keyVaultName - } -} - -module storageAccount '../storage/storage-account.bicep' = { - name: 'storageAccount' - params: { - location: location - tags: tags - name: storageAccountName - containers: [ - { - name: 'default' - } - ] - files: [ - { - name: 'default' - } - ] - queues: [ - { - name: 'default' - } - ] - tables: [ - { - name: 'default' - } - ] - corsRules: [ - { - allowedOrigins: [ - 'https://mlworkspace.azure.ai' - 'https://ml.azure.com' - 'https://*.ml.azure.com' - 'https://ai.azure.com' - 'https://*.ai.azure.com' - 'https://mlworkspacecanary.azure.ai' - 'https://mlworkspace.azureml-test.net' - ] - allowedMethods: [ - 'GET' - 'HEAD' - 'POST' - 'PUT' - 'DELETE' - 'OPTIONS' - 'PATCH' - ] - maxAgeInSeconds: 1800 - exposedHeaders: [ - '*' - ] - allowedHeaders: [ - '*' - ] - } - ] - deleteRetentionPolicy: { - allowPermanentDelete: false - enabled: false - } - shareDeleteRetentionPolicy: { - enabled: true - days: 7 - } - } -} - -module logAnalytics '../monitor/loganalytics.bicep' = - if (!empty(logAnalyticsName)) { - name: 'logAnalytics' - params: { - location: location - tags: tags - name: logAnalyticsName - } - } - -module applicationInsights '../monitor/applicationinsights.bicep' = - if (!empty(applicationInsightsName) && !empty(logAnalyticsName)) { - name: 'applicationInsights' - params: { - location: location - tags: tags - name: applicationInsightsName - logAnalyticsWorkspaceId: !empty(logAnalyticsName) ? logAnalytics.outputs.id : '' - } - } - -module containerRegistry '../host/container-registry.bicep' = - if (!empty(containerRegistryName)) { - name: 'containerRegistry' - params: { - location: location - tags: tags - name: containerRegistryName - } - } - -module cognitiveServices '../ai/cognitiveservices.bicep' = { - name: 'cognitiveServices' - params: { - location: location - tags: tags - name: openAiName - kind: 'AIServices' - deployments: openAiModelDeployments - } -} - -module searchService '../search/search-services.bicep' = - if (!empty(searchServiceName)) { - name: 'searchService' - params: { - location: location - tags: tags - name: searchServiceName - } - } - -output keyVaultId string = keyVault.outputs.id -output keyVaultName string = keyVault.outputs.name -output keyVaultEndpoint string = keyVault.outputs.endpoint - -output storageAccountId string = storageAccount.outputs.id -output storageAccountName string = storageAccount.outputs.name - -output containerRegistryId string = !empty(containerRegistryName) ? containerRegistry.outputs.id : '' -output containerRegistryName string = !empty(containerRegistryName) ? containerRegistry.outputs.name : '' -output containerRegistryEndpoint string = !empty(containerRegistryName) ? containerRegistry.outputs.loginServer : '' - -output applicationInsightsId string = !empty(applicationInsightsName) ? applicationInsights.outputs.id : '' -output applicationInsightsName string = !empty(applicationInsightsName) ? applicationInsights.outputs.name : '' -output logAnalyticsWorkspaceId string = !empty(logAnalyticsName) ? logAnalytics.outputs.id : '' -output logAnalyticsWorkspaceName string = !empty(logAnalyticsName) ? logAnalytics.outputs.name : '' - -output openAiId string = cognitiveServices.outputs.id -output openAiName string = cognitiveServices.outputs.name -output openAiEndpoint string = cognitiveServices.outputs.endpoints['OpenAI Language Model Instance API'] - -output searchServiceId string = !empty(searchServiceName) ? searchService.outputs.id : '' -output searchServiceName string = !empty(searchServiceName) ? searchService.outputs.name : '' -output searchServiceEndpoint string = !empty(searchServiceName) ? searchService.outputs.endpoint : '' diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/ai/hub.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/ai/hub.bicep deleted file mode 100644 index 576d13c..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/ai/hub.bicep +++ /dev/null @@ -1,113 +0,0 @@ -@description('The AI Studio Hub Resource name') -param name string -@description('The display name of the AI Studio Hub Resource') -param displayName string = name -@description('The storage account ID to use for the AI Studio Hub Resource') -param storageAccountId string -@description('The key vault ID to use for the AI Studio Hub Resource') -param keyVaultId string -@description('The application insights ID to use for the AI Studio Hub Resource') -param applicationInsightsId string = '' -@description('The container registry ID to use for the AI Studio Hub Resource') -param containerRegistryId string = '' -@description('The OpenAI Cognitive Services account name to use for the AI Studio Hub Resource') -param openAiName string -@description('The OpenAI Cognitive Services account connection name to use for the AI Studio Hub Resource') -param openAiConnectionName string -@description('The Azure Cognitive Search service name to use for the AI Studio Hub Resource') -param aiSearchName string = '' -@description('The Azure Cognitive Search service connection name to use for the AI Studio Hub Resource') -param aiSearchConnectionName string - -@description('The SKU name to use for the AI Studio Hub Resource') -param skuName string = 'Basic' -@description('The SKU tier to use for the AI Studio Hub Resource') -@allowed(['Basic', 'Free', 'Premium', 'Standard']) -param skuTier string = 'Basic' -@description('The public network access setting to use for the AI Studio Hub Resource') -@allowed(['Enabled','Disabled']) -param publicNetworkAccess string = 'Enabled' - -param location string = resourceGroup().location -param tags object = {} - -resource hub 'Microsoft.MachineLearningServices/workspaces@2024-01-01-preview' = { - name: name - location: location - tags: tags - sku: { - name: skuName - tier: skuTier - } - kind: 'Hub' - identity: { - type: 'SystemAssigned' - } - properties: { - friendlyName: displayName - storageAccount: storageAccountId - keyVault: keyVaultId - applicationInsights: !empty(applicationInsightsId) ? applicationInsightsId : null - containerRegistry: !empty(containerRegistryId) ? containerRegistryId : null - hbiWorkspace: false - managedNetwork: { - isolationMode: 'Disabled' - } - v1LegacyMode: false - publicNetworkAccess: publicNetworkAccess - } - - resource contentSafetyDefaultEndpoint 'endpoints' = { - name: 'Azure.ContentSafety' - properties: { - name: 'Azure.ContentSafety' - endpointType: 'Azure.ContentSafety' - associatedResourceId: openAi.id - } - } - - resource openAiConnection 'connections' = { - name: openAiConnectionName - properties: { - category: 'AzureOpenAI' - authType: 'ApiKey' - isSharedToAll: true - target: openAi.properties.endpoints['OpenAI Language Model Instance API'] - metadata: { - ApiVersion: '2023-07-01-preview' - ApiType: 'azure' - ResourceId: openAi.id - } - credentials: { - key: openAi.listKeys().key1 - } - } - } - - resource searchConnection 'connections' = - if (!empty(aiSearchName)) { - name: aiSearchConnectionName - properties: { - category: 'CognitiveSearch' - authType: 'ApiKey' - isSharedToAll: true - target: 'https://${search.name}.search.windows.net/' - credentials: { - key: !empty(aiSearchName) ? search.listAdminKeys().primaryKey : '' - } - } - } -} - -resource openAi 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = { - name: openAiName -} - -resource search 'Microsoft.Search/searchServices@2021-04-01-preview' existing = - if (!empty(aiSearchName)) { - name: aiSearchName - } - -output name string = hub.name -output id string = hub.id -output principalId string = hub.identity.principalId diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/ai/project.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/ai/project.bicep deleted file mode 100644 index 78b0d52..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/ai/project.bicep +++ /dev/null @@ -1,75 +0,0 @@ -@description('The AI Studio Hub Resource name') -param name string -@description('The display name of the AI Studio Hub Resource') -param displayName string = name -@description('The name of the AI Studio Hub Resource where this project should be created') -param hubName string -@description('The name of the key vault resource to grant access to the project') -param keyVaultName string - -@description('The SKU name to use for the AI Studio Hub Resource') -param skuName string = 'Basic' -@description('The SKU tier to use for the AI Studio Hub Resource') -@allowed(['Basic', 'Free', 'Premium', 'Standard']) -param skuTier string = 'Basic' -@description('The public network access setting to use for the AI Studio Hub Resource') -@allowed(['Enabled','Disabled']) -param publicNetworkAccess string = 'Enabled' - -param location string = resourceGroup().location -param tags object = {} - -resource project 'Microsoft.MachineLearningServices/workspaces@2024-01-01-preview' = { - name: name - location: location - tags: tags - sku: { - name: skuName - tier: skuTier - } - kind: 'Project' - identity: { - type: 'SystemAssigned' - } - properties: { - friendlyName: displayName - hbiWorkspace: false - v1LegacyMode: false - publicNetworkAccess: publicNetworkAccess - hubResourceId: hub.id - } -} - -module keyVaultAccess '../security/keyvault-access.bicep' = { - name: 'keyvault-access' - params: { - keyVaultName: keyVaultName - principalId: project.identity.principalId - } -} - -module mlServiceRoleDataScientist '../security/role.bicep' = { - name: 'ml-service-role-data-scientist' - params: { - principalId: project.identity.principalId - roleDefinitionId: 'f6c7c914-8db3-469d-8ca1-694a8f32e121' - principalType: 'ServicePrincipal' - } -} - -module mlServiceRoleSecretsReader '../security/role.bicep' = { - name: 'ml-service-role-secrets-reader' - params: { - principalId: project.identity.principalId - roleDefinitionId: 'ea01e6af-a1c1-4350-9563-ad00f8c72ec5' - principalType: 'ServicePrincipal' - } -} - -resource hub 'Microsoft.MachineLearningServices/workspaces@2024-01-01-preview' existing = { - name: hubName -} - -output id string = project.id -output name string = project.name -output principalId string = project.identity.principalId diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/config/configstore.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/config/configstore.bicep deleted file mode 100644 index 96818f1..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/config/configstore.bicep +++ /dev/null @@ -1,48 +0,0 @@ -metadata description = 'Creates an Azure App Configuration store.' - -@description('The name for the Azure App Configuration store') -param name string - -@description('The Azure region/location for the Azure App Configuration store') -param location string = resourceGroup().location - -@description('Custom tags to apply to the Azure App Configuration store') -param tags object = {} - -@description('Specifies the names of the key-value resources. The name is a combination of key and label with $ as delimiter. The label is optional.') -param keyValueNames array = [] - -@description('Specifies the values of the key-value resources.') -param keyValueValues array = [] - -@description('The principal ID to grant access to the Azure App Configuration store') -param principalId string - -resource configStore 'Microsoft.AppConfiguration/configurationStores@2023-03-01' = { - name: name - location: location - sku: { - name: 'standard' - } - tags: tags -} - -resource configStoreKeyValue 'Microsoft.AppConfiguration/configurationStores/keyValues@2023-03-01' = [for (item, i) in keyValueNames: { - parent: configStore - name: item - properties: { - value: keyValueValues[i] - tags: tags - } -}] - -module configStoreAccess '../security/configstore-access.bicep' = { - name: 'app-configuration-access' - params: { - configStoreName: name - principalId: principalId - } - dependsOn: [configStore] -} - -output endpoint string = configStore.properties.endpoint diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/cosmos-account.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/cosmos-account.bicep deleted file mode 100644 index c16b229..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/cosmos-account.bicep +++ /dev/null @@ -1,50 +0,0 @@ -metadata description = 'Creates an Azure Cosmos DB account.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' -param keyVaultName string - -@allowed([ 'GlobalDocumentDB', 'MongoDB', 'Parse' ]) -param kind string - -resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2023-11-15' = { - name: name - kind: kind - location: location - tags: tags - properties: { - consistencyPolicy: { defaultConsistencyLevel: 'Session' } - locations: [ - { - locationName: location - failoverPriority: 0 - isZoneRedundant: false - } - ] - databaseAccountOfferType: 'Standard' - enableAutomaticFailover: false - enableMultipleWriteLocations: false - apiProperties: (kind == 'MongoDB') ? { serverVersion: '4.2' } : {} - capabilities: [ { name: 'EnableServerless' } ] - minimalTlsVersion: 'Tls12' - } -} - -resource cosmosConnectionString 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: connectionStringKey - properties: { - value: cosmos.listConnectionStrings().connectionStrings[0].connectionString - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} - -output connectionStringKey string = connectionStringKey -output endpoint string = cosmos.properties.documentEndpoint -output id string = cosmos.id -output name string = cosmos.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep deleted file mode 100644 index 4aafbf3..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep +++ /dev/null @@ -1,23 +0,0 @@ -metadata description = 'Creates an Azure Cosmos DB for MongoDB account.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param keyVaultName string -param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' - -module cosmos '../../cosmos/cosmos-account.bicep' = { - name: 'cosmos-account' - params: { - name: name - location: location - connectionStringKey: connectionStringKey - keyVaultName: keyVaultName - kind: 'MongoDB' - tags: tags - } -} - -output connectionStringKey string = cosmos.outputs.connectionStringKey -output endpoint string = cosmos.outputs.endpoint -output id string = cosmos.outputs.id diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep deleted file mode 100644 index 2a67057..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep +++ /dev/null @@ -1,47 +0,0 @@ -metadata description = 'Creates an Azure Cosmos DB for MongoDB account with a database.' -param accountName string -param databaseName string -param location string = resourceGroup().location -param tags object = {} - -param collections array = [] -param connectionStringKey string = 'AZURE-COSMOS-CONNECTION-STRING' -param keyVaultName string - -module cosmos 'cosmos-mongo-account.bicep' = { - name: 'cosmos-mongo-account' - params: { - name: accountName - location: location - keyVaultName: keyVaultName - tags: tags - connectionStringKey: connectionStringKey - } -} - -resource database 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2022-08-15' = { - name: '${accountName}/${databaseName}' - tags: tags - properties: { - resource: { id: databaseName } - } - - resource list 'collections' = [for collection in collections: { - name: collection.name - properties: { - resource: { - id: collection.id - shardKey: { _id: collection.shardKey } - indexes: [ { key: { keys: [ collection.indexKey ] } } ] - } - } - }] - - dependsOn: [ - cosmos - ] -} - -output connectionStringKey string = connectionStringKey -output databaseName string = databaseName -output endpoint string = cosmos.outputs.endpoint diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-account.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-account.bicep deleted file mode 100644 index 8431135..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-account.bicep +++ /dev/null @@ -1,22 +0,0 @@ -metadata description = 'Creates an Azure Cosmos DB for NoSQL account.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param keyVaultName string - -module cosmos '../../cosmos/cosmos-account.bicep' = { - name: 'cosmos-account' - params: { - name: name - location: location - tags: tags - keyVaultName: keyVaultName - kind: 'GlobalDocumentDB' - } -} - -output connectionStringKey string = cosmos.outputs.connectionStringKey -output endpoint string = cosmos.outputs.endpoint -output id string = cosmos.outputs.id -output name string = cosmos.outputs.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-db.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-db.bicep deleted file mode 100644 index 265880d..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-db.bicep +++ /dev/null @@ -1,74 +0,0 @@ -metadata description = 'Creates an Azure Cosmos DB for NoSQL account with a database.' -param accountName string -param databaseName string -param location string = resourceGroup().location -param tags object = {} - -param containers array = [] -param keyVaultName string -param principalIds array = [] - -module cosmos 'cosmos-sql-account.bicep' = { - name: 'cosmos-sql-account' - params: { - name: accountName - location: location - tags: tags - keyVaultName: keyVaultName - } -} - -resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2022-05-15' = { - name: '${accountName}/${databaseName}' - properties: { - resource: { id: databaseName } - } - - resource list 'containers' = [for container in containers: { - name: container.name - properties: { - resource: { - id: container.id - partitionKey: { paths: [ container.partitionKey ] } - } - options: {} - } - }] - - dependsOn: [ - cosmos - ] -} - -module roleDefinition 'cosmos-sql-role-def.bicep' = { - name: 'cosmos-sql-role-definition' - params: { - accountName: accountName - } - dependsOn: [ - cosmos - database - ] -} - -// We need batchSize(1) here because sql role assignments have to be done sequentially -@batchSize(1) -module userRole 'cosmos-sql-role-assign.bicep' = [for principalId in principalIds: if (!empty(principalId)) { - name: 'cosmos-sql-user-role-${uniqueString(principalId)}' - params: { - accountName: accountName - roleDefinitionId: roleDefinition.outputs.id - principalId: principalId - } - dependsOn: [ - cosmos - database - ] -}] - -output accountId string = cosmos.outputs.id -output accountName string = cosmos.outputs.name -output connectionStringKey string = cosmos.outputs.connectionStringKey -output databaseName string = databaseName -output endpoint string = cosmos.outputs.endpoint -output roleDefinitionId string = roleDefinition.outputs.id diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep deleted file mode 100644 index 3949efe..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep +++ /dev/null @@ -1,19 +0,0 @@ -metadata description = 'Creates a SQL role assignment under an Azure Cosmos DB account.' -param accountName string - -param roleDefinitionId string -param principalId string = '' - -resource role 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2022-05-15' = { - parent: cosmos - name: guid(roleDefinitionId, principalId, cosmos.id) - properties: { - principalId: principalId - roleDefinitionId: roleDefinitionId - scope: cosmos.id - } -} - -resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = { - name: accountName -} diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep deleted file mode 100644 index 778d6dc..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep +++ /dev/null @@ -1,30 +0,0 @@ -metadata description = 'Creates a SQL role definition under an Azure Cosmos DB account.' -param accountName string - -resource roleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2022-08-15' = { - parent: cosmos - name: guid(cosmos.id, accountName, 'sql-role') - properties: { - assignableScopes: [ - cosmos.id - ] - permissions: [ - { - dataActions: [ - 'Microsoft.DocumentDB/databaseAccounts/readMetadata' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' - ] - notDataActions: [] - } - ] - roleName: 'Reader Writer' - type: 'CustomRole' - } -} - -resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2022-08-15' existing = { - name: accountName -} - -output id string = roleDefinition.id diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/database/mysql/flexibleserver.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/database/mysql/flexibleserver.bicep deleted file mode 100644 index 8319f1c..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/database/mysql/flexibleserver.bicep +++ /dev/null @@ -1,65 +0,0 @@ -metadata description = 'Creates an Azure Database for MySQL - Flexible Server.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object -param storage object -param administratorLogin string -@secure() -param administratorLoginPassword string -param highAvailabilityMode string = 'Disabled' -param databaseNames array = [] -param allowAzureIPsFirewall bool = false -param allowAllIPsFirewall bool = false -param allowedSingleIPs array = [] - -// MySQL version -param version string - -resource mysqlServer 'Microsoft.DBforMySQL/flexibleServers@2023-06-30' = { - location: location - tags: tags - name: name - sku: sku - properties: { - version: version - administratorLogin: administratorLogin - administratorLoginPassword: administratorLoginPassword - storage: storage - highAvailability: { - mode: highAvailabilityMode - } - } - - resource database 'databases' = [for name in databaseNames: { - name: name - }] - - resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) { - name: 'allow-all-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '255.255.255.255' - } - } - - resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) { - name: 'allow-all-azure-internal-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } - } - - resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: { - name: 'allow-single-${replace(ip, '.', '')}' - properties: { - startIpAddress: ip - endIpAddress: ip - } - }] - -} - -output MYSQL_DOMAIN_NAME string = mysqlServer.properties.fullyQualifiedDomainName diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/database/postgresql/flexibleserver.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/database/postgresql/flexibleserver.bicep deleted file mode 100644 index 7e26b1a..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/database/postgresql/flexibleserver.bicep +++ /dev/null @@ -1,65 +0,0 @@ -metadata description = 'Creates an Azure Database for PostgreSQL - Flexible Server.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object -param storage object -param administratorLogin string -@secure() -param administratorLoginPassword string -param databaseNames array = [] -param allowAzureIPsFirewall bool = false -param allowAllIPsFirewall bool = false -param allowedSingleIPs array = [] - -// PostgreSQL version -param version string - -// Latest official version 2022-12-01 does not have Bicep types available -resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = { - location: location - tags: tags - name: name - sku: sku - properties: { - version: version - administratorLogin: administratorLogin - administratorLoginPassword: administratorLoginPassword - storage: storage - highAvailability: { - mode: 'Disabled' - } - } - - resource database 'databases' = [for name in databaseNames: { - name: name - }] - - resource firewall_all 'firewallRules' = if (allowAllIPsFirewall) { - name: 'allow-all-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '255.255.255.255' - } - } - - resource firewall_azure 'firewallRules' = if (allowAzureIPsFirewall) { - name: 'allow-all-azure-internal-IPs' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } - } - - resource firewall_single 'firewallRules' = [for ip in allowedSingleIPs: { - name: 'allow-single-${replace(ip, '.', '')}' - properties: { - startIpAddress: ip - endIpAddress: ip - } - }] - -} - -output POSTGRES_DOMAIN_NAME string = postgresServer.properties.fullyQualifiedDomainName diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/database/sqlserver/sqlserver.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/database/sqlserver/sqlserver.bicep deleted file mode 100644 index 84f2cc2..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/database/sqlserver/sqlserver.bicep +++ /dev/null @@ -1,130 +0,0 @@ -metadata description = 'Creates an Azure SQL Server instance.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param appUser string = 'appUser' -param databaseName string -param keyVaultName string -param sqlAdmin string = 'sqlAdmin' -param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING' - -@secure() -param sqlAdminPassword string -@secure() -param appUserPassword string - -resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = { - name: name - location: location - tags: tags - properties: { - version: '12.0' - minimalTlsVersion: '1.2' - publicNetworkAccess: 'Enabled' - administratorLogin: sqlAdmin - administratorLoginPassword: sqlAdminPassword - } - - resource database 'databases' = { - name: databaseName - location: location - } - - resource firewall 'firewallRules' = { - name: 'Azure Services' - properties: { - // Allow all clients - // Note: range [0.0.0.0-0.0.0.0] means "allow all Azure-hosted clients only". - // This is not sufficient, because we also want to allow direct access from developer machine, for debugging purposes. - startIpAddress: '0.0.0.1' - endIpAddress: '255.255.255.254' - } - } -} - -resource sqlDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { - name: '${name}-deployment-script' - location: location - kind: 'AzureCLI' - properties: { - azCliVersion: '2.37.0' - retentionInterval: 'PT1H' // Retain the script resource for 1 hour after it ends running - timeout: 'PT5M' // Five minutes - cleanupPreference: 'OnSuccess' - environmentVariables: [ - { - name: 'APPUSERNAME' - value: appUser - } - { - name: 'APPUSERPASSWORD' - secureValue: appUserPassword - } - { - name: 'DBNAME' - value: databaseName - } - { - name: 'DBSERVER' - value: sqlServer.properties.fullyQualifiedDomainName - } - { - name: 'SQLCMDPASSWORD' - secureValue: sqlAdminPassword - } - { - name: 'SQLADMIN' - value: sqlAdmin - } - ] - - scriptContent: ''' -wget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8.1-linux-x64.tar.bz2 -tar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C . - -cat < ./initDb.sql -drop user if exists ${APPUSERNAME} -go -create user ${APPUSERNAME} with password = '${APPUSERPASSWORD}' -go -alter role db_owner add member ${APPUSERNAME} -go -SCRIPT_END - -./sqlcmd -S ${DBSERVER} -d ${DBNAME} -U ${SQLADMIN} -i ./initDb.sql - ''' - } -} - -resource sqlAdminPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: 'sqlAdminPassword' - properties: { - value: sqlAdminPassword - } -} - -resource appUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: 'appUserPassword' - properties: { - value: appUserPassword - } -} - -resource sqlAzureConnectionStringSercret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - parent: keyVault - name: connectionStringKey - properties: { - value: '${connectionString}; Password=${appUserPassword}' - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} - -var connectionString = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlServer::database.name}; User=${appUser}' -output connectionStringKey string = connectionStringKey -output databaseName string = sqlServer::database.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/gateway/apim.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/gateway/apim.bicep deleted file mode 100644 index be7464f..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/gateway/apim.bicep +++ /dev/null @@ -1,79 +0,0 @@ -metadata description = 'Creates an Azure API Management instance.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('The email address of the owner of the service') -@minLength(1) -param publisherEmail string = 'noreply@microsoft.com' - -@description('The name of the owner of the service') -@minLength(1) -param publisherName string = 'n/a' - -@description('The pricing tier of this API Management service') -@allowed([ - 'Consumption' - 'Developer' - 'Standard' - 'Premium' -]) -param sku string = 'Consumption' - -@description('The instance size of this API Management service.') -@allowed([ 0, 1, 2 ]) -param skuCount int = 0 - -@description('Azure Application Insights Name') -param applicationInsightsName string - -resource apimService 'Microsoft.ApiManagement/service@2021-08-01' = { - name: name - location: location - tags: union(tags, { 'azd-service-name': name }) - sku: { - name: sku - capacity: (sku == 'Consumption') ? 0 : ((sku == 'Developer') ? 1 : skuCount) - } - properties: { - publisherEmail: publisherEmail - publisherName: publisherName - // Custom properties are not supported for Consumption SKU - customProperties: sku == 'Consumption' ? {} : { - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_GCM_SHA256': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA256': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA256': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_256_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_RSA_WITH_AES_128_CBC_SHA': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TripleDes168': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Ssl30': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11': 'false' - 'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30': 'false' - } - } -} - -resource apimLogger 'Microsoft.ApiManagement/service/loggers@2021-12-01-preview' = if (!empty(applicationInsightsName)) { - name: 'app-insights-logger' - parent: apimService - properties: { - credentials: { - instrumentationKey: applicationInsights.properties.InstrumentationKey - } - description: 'Logger to Azure Application Insights' - isBuffered: false - loggerType: 'applicationInsights' - resourceId: applicationInsights.id - } -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { - name: applicationInsightsName -} - -output apimServiceName string = apimService.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/ai-environment.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/ai-environment.bicep deleted file mode 100644 index d03675f..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/ai-environment.bicep +++ /dev/null @@ -1,110 +0,0 @@ -@minLength(1) -@description('Primary location for all resources') -param location string - -@description('The AI Hub resource name.') -param hubName string -@description('The AI Project resource name.') -param projectName string -@description('The Key Vault resource name.') -param keyVaultName string -@description('The Storage Account resource name.') -param storageAccountName string -@description('The Open AI resource name.') -param openAiName string -@description('The Open AI connection name.') -param openAiConnectionName string -@description('The Open AI model deployments.') -param openAiModelDeployments array = [] -@description('The Log Analytics resource name.') -param logAnalyticsName string = '' -@description('The Application Insights resource name.') -param applicationInsightsName string = '' -@description('The Container Registry resource name.') -param containerRegistryName string = '' -@description('The Azure Search resource name.') -param searchServiceName string = '' -@description('The Azure Search connection name.') -param searchConnectionName string = '' -param tags object = {} - -module hubDependencies '../ai/hub-dependencies.bicep' = { - name: 'hubDependencies' - params: { - location: location - tags: tags - keyVaultName: keyVaultName - storageAccountName: storageAccountName - containerRegistryName: containerRegistryName - applicationInsightsName: applicationInsightsName - logAnalyticsName: logAnalyticsName - openAiName: openAiName - openAiModelDeployments: openAiModelDeployments - searchServiceName: searchServiceName - } -} - -module hub '../ai/hub.bicep' = { - name: 'hub' - params: { - location: location - tags: tags - name: hubName - displayName: hubName - keyVaultId: hubDependencies.outputs.keyVaultId - storageAccountId: hubDependencies.outputs.storageAccountId - containerRegistryId: hubDependencies.outputs.containerRegistryId - applicationInsightsId: hubDependencies.outputs.applicationInsightsId - openAiName: hubDependencies.outputs.openAiName - openAiConnectionName: openAiConnectionName - aiSearchName: hubDependencies.outputs.searchServiceName - aiSearchConnectionName: searchConnectionName - } -} - -module project '../ai/project.bicep' = { - name: 'project' - params: { - location: location - tags: tags - name: projectName - displayName: projectName - hubName: hub.outputs.name - keyVaultName: hubDependencies.outputs.keyVaultName - } -} - -// Outputs -// Resource Group -output resourceGroupName string = resourceGroup().name - -// Hub -output hubName string = hub.outputs.name -output hubPrincipalId string = hub.outputs.principalId - -// Project -output projectName string = project.outputs.name -output projectPrincipalId string = project.outputs.principalId - -// Key Vault -output keyVaultName string = hubDependencies.outputs.keyVaultName -output keyVaultEndpoint string = hubDependencies.outputs.keyVaultEndpoint - -// Application Insights -output applicationInsightsName string = hubDependencies.outputs.applicationInsightsName -output logAnalyticsWorkspaceName string = hubDependencies.outputs.logAnalyticsWorkspaceName - -// Container Registry -output containerRegistryName string = hubDependencies.outputs.containerRegistryName -output containerRegistryEndpoint string = hubDependencies.outputs.containerRegistryEndpoint - -// Storage Account -output storageAccountName string = hubDependencies.outputs.storageAccountName - -// Open AI -output openAiName string = hubDependencies.outputs.openAiName -output openAiEndpoint string = hubDependencies.outputs.openAiEndpoint - -// Search -output searchServiceName string = hubDependencies.outputs.searchServiceName -output searchServiceEndpoint string = hubDependencies.outputs.searchServiceEndpoint diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/aks-agent-pool.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/aks-agent-pool.bicep deleted file mode 100644 index 9c76435..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/aks-agent-pool.bicep +++ /dev/null @@ -1,18 +0,0 @@ -metadata description = 'Adds an agent pool to an Azure Kubernetes Service (AKS) cluster.' -param clusterName string - -@description('The agent pool name') -param name string - -@description('The agent pool configuration') -param config object - -resource aksCluster 'Microsoft.ContainerService/managedClusters@2023-10-02-preview' existing = { - name: clusterName -} - -resource nodePool 'Microsoft.ContainerService/managedClusters/agentPools@2023-10-02-preview' = { - parent: aksCluster - name: name - properties: config -} diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/aks-managed-cluster.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/aks-managed-cluster.bicep deleted file mode 100644 index de562a6..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/aks-managed-cluster.bicep +++ /dev/null @@ -1,140 +0,0 @@ -metadata description = 'Creates an Azure Kubernetes Service (AKS) cluster with a system agent pool.' -@description('The name for the AKS managed cluster') -param name string - -@description('The name of the resource group for the managed resources of the AKS cluster') -param nodeResourceGroupName string = '' - -@description('The Azure region/location for the AKS resources') -param location string = resourceGroup().location - -@description('Custom tags to apply to the AKS resources') -param tags object = {} - -@description('Kubernetes Version') -param kubernetesVersion string = '1.27.7' - -@description('Whether RBAC is enabled for local accounts') -param enableRbac bool = true - -// Add-ons -@description('Whether web app routing (preview) add-on is enabled') -param webAppRoutingAddon bool = true - -// AAD Integration -@description('Enable Azure Active Directory integration') -param enableAad bool = false - -@description('Enable RBAC using AAD') -param enableAzureRbac bool = false - -@description('The Tenant ID associated to the Azure Active Directory') -param aadTenantId string = tenant().tenantId - -@description('The load balancer SKU to use for ingress into the AKS cluster') -@allowed([ 'basic', 'standard' ]) -param loadBalancerSku string = 'standard' - -@description('Network plugin used for building the Kubernetes network.') -@allowed([ 'azure', 'kubenet', 'none' ]) -param networkPlugin string = 'azure' - -@description('Network policy used for building the Kubernetes network.') -@allowed([ 'azure', 'calico' ]) -param networkPolicy string = 'azure' - -@description('If set to true, getting static credentials will be disabled for this cluster.') -param disableLocalAccounts bool = false - -@description('The managed cluster SKU.') -@allowed([ 'Free', 'Paid', 'Standard' ]) -param sku string = 'Free' - -@description('Configuration of AKS add-ons') -param addOns object = {} - -@description('The log analytics workspace id used for logging & monitoring') -param workspaceId string = '' - -@description('The node pool configuration for the System agent pool') -param systemPoolConfig object - -@description('The DNS prefix to associate with the AKS cluster') -param dnsPrefix string = '' - -resource aks 'Microsoft.ContainerService/managedClusters@2023-10-02-preview' = { - name: name - location: location - tags: tags - identity: { - type: 'SystemAssigned' - } - sku: { - name: 'Base' - tier: sku - } - properties: { - nodeResourceGroup: !empty(nodeResourceGroupName) ? nodeResourceGroupName : 'rg-mc-${name}' - kubernetesVersion: kubernetesVersion - dnsPrefix: empty(dnsPrefix) ? '${name}-dns' : dnsPrefix - enableRBAC: enableRbac - aadProfile: enableAad ? { - managed: true - enableAzureRBAC: enableAzureRbac - tenantID: aadTenantId - } : null - agentPoolProfiles: [ - systemPoolConfig - ] - networkProfile: { - loadBalancerSku: loadBalancerSku - networkPlugin: networkPlugin - networkPolicy: networkPolicy - } - disableLocalAccounts: disableLocalAccounts && enableAad - addonProfiles: addOns - ingressProfile: { - webAppRouting: { - enabled: webAppRoutingAddon - } - } - } -} - -var aksDiagCategories = [ - 'cluster-autoscaler' - 'kube-controller-manager' - 'kube-audit-admin' - 'guard' -] - -// TODO: Update diagnostics to be its own module -// Blocking issue: https://github.com/Azure/bicep/issues/622 -// Unable to pass in a `resource` scope or unable to use string interpolation in resource types -resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { - name: 'aks-diagnostics' - scope: aks - properties: { - workspaceId: workspaceId - logs: [for category in aksDiagCategories: { - category: category - enabled: true - }] - metrics: [ - { - category: 'AllMetrics' - enabled: true - } - ] - } -} - -@description('The resource name of the AKS cluster') -output clusterName string = aks.name - -@description('The AKS cluster identity') -output clusterIdentity object = { - clientId: aks.properties.identityProfile.kubeletidentity.clientId - objectId: aks.properties.identityProfile.kubeletidentity.objectId - resourceId: aks.properties.identityProfile.kubeletidentity.resourceId -} diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/aks.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/aks.bicep deleted file mode 100644 index 15bb7cb..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/aks.bicep +++ /dev/null @@ -1,285 +0,0 @@ -metadata description = 'Creates an Azure Kubernetes Service (AKS) cluster with a system agent pool as well as an additional user agent pool.' -@description('The name for the AKS managed cluster') -param name string - -@description('The name for the Azure container registry (ACR)') -param containerRegistryName string - -@description('The name of the connected log analytics workspace') -param logAnalyticsName string = '' - -@description('The name of the keyvault to grant access') -param keyVaultName string - -@description('The Azure region/location for the AKS resources') -param location string = resourceGroup().location - -@description('Custom tags to apply to the AKS resources') -param tags object = {} - -@description('AKS add-ons configuration') -param addOns object = { - azurePolicy: { - enabled: true - config: { - version: 'v2' - } - } - keyVault: { - enabled: true - config: { - enableSecretRotation: 'true' - rotationPollInterval: '2m' - } - } - openServiceMesh: { - enabled: false - config: {} - } - omsAgent: { - enabled: true - config: {} - } - applicationGateway: { - enabled: false - config: {} - } -} - -@description('The managed cluster SKU.') -@allowed([ 'Free', 'Paid', 'Standard' ]) -param sku string = 'Free' - -@description('The load balancer SKU to use for ingress into the AKS cluster') -@allowed([ 'basic', 'standard' ]) -param loadBalancerSku string = 'standard' - -@description('Network plugin used for building the Kubernetes network.') -@allowed([ 'azure', 'kubenet', 'none' ]) -param networkPlugin string = 'azure' - -@description('Network policy used for building the Kubernetes network.') -@allowed([ 'azure', 'calico' ]) -param networkPolicy string = 'azure' - -@description('The DNS prefix to associate with the AKS cluster') -param dnsPrefix string = '' - -@description('The name of the resource group for the managed resources of the AKS cluster') -param nodeResourceGroupName string = '' - -@allowed([ - 'CostOptimised' - 'Standard' - 'HighSpec' - 'Custom' -]) -@description('The System Pool Preset sizing') -param systemPoolType string = 'CostOptimised' - -@allowed([ - '' - 'CostOptimised' - 'Standard' - 'HighSpec' - 'Custom' -]) -@description('The User Pool Preset sizing') -param agentPoolType string = '' - -// Configure system / user agent pools -@description('Custom configuration of system node pool') -param systemPoolConfig object = {} -@description('Custom configuration of user node pool') -param agentPoolConfig object = {} - -@description('Id of the user or app to assign application roles') -param principalId string = '' - -@description('The type of principal to assign application roles') -@allowed(['Device','ForeignGroup','Group','ServicePrincipal','User']) -param principalType string = 'User' - -@description('Kubernetes Version') -param kubernetesVersion string = '1.27' - -@description('The Tenant ID associated to the Azure Active Directory') -param aadTenantId string = tenant().tenantId - -@description('Whether RBAC is enabled for local accounts') -param enableRbac bool = true - -@description('If set to true, getting static credentials will be disabled for this cluster.') -param disableLocalAccounts bool = false - -@description('Enable RBAC using AAD') -param enableAzureRbac bool = false - -// Add-ons -@description('Whether web app routing (preview) add-on is enabled') -param webAppRoutingAddon bool = true - -// Configure AKS add-ons -var omsAgentConfig = (!empty(logAnalyticsName) && !empty(addOns.omsAgent) && addOns.omsAgent.enabled) ? union( - addOns.omsAgent, - { - config: { - logAnalyticsWorkspaceResourceID: logAnalytics.id - } - } -) : {} - -var addOnsConfig = union( - (!empty(addOns.azurePolicy) && addOns.azurePolicy.enabled) ? { azurepolicy: addOns.azurePolicy } : {}, - (!empty(addOns.keyVault) && addOns.keyVault.enabled) ? { azureKeyvaultSecretsProvider: addOns.keyVault } : {}, - (!empty(addOns.openServiceMesh) && addOns.openServiceMesh.enabled) ? { openServiceMesh: addOns.openServiceMesh } : {}, - (!empty(addOns.omsAgent) && addOns.omsAgent.enabled) ? { omsagent: omsAgentConfig } : {}, - (!empty(addOns.applicationGateway) && addOns.applicationGateway.enabled) ? { ingressApplicationGateway: addOns.applicationGateway } : {} -) - -// Link to existing log analytics workspace when available -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' existing = if (!empty(logAnalyticsName)) { - name: logAnalyticsName -} - -var systemPoolSpec = !empty(systemPoolConfig) ? systemPoolConfig : nodePoolPresets[systemPoolType] - -// Create the primary AKS cluster resources and system node pool -module managedCluster 'aks-managed-cluster.bicep' = { - name: 'managed-cluster' - params: { - name: name - location: location - tags: tags - systemPoolConfig: union( - { name: 'npsystem', mode: 'System' }, - nodePoolBase, - systemPoolSpec - ) - nodeResourceGroupName: nodeResourceGroupName - sku: sku - dnsPrefix: dnsPrefix - kubernetesVersion: kubernetesVersion - addOns: addOnsConfig - workspaceId: !empty(logAnalyticsName) ? logAnalytics.id : '' - enableAad: enableAzureRbac && aadTenantId != '' - disableLocalAccounts: disableLocalAccounts - aadTenantId: aadTenantId - enableRbac: enableRbac - enableAzureRbac: enableAzureRbac - webAppRoutingAddon: webAppRoutingAddon - loadBalancerSku: loadBalancerSku - networkPlugin: networkPlugin - networkPolicy: networkPolicy - } -} - -var hasAgentPool = !empty(agentPoolConfig) || !empty(agentPoolType) -var agentPoolSpec = hasAgentPool && !empty(agentPoolConfig) ? agentPoolConfig : empty(agentPoolType) ? {} : nodePoolPresets[agentPoolType] - -// Create additional user agent pool when specified -module agentPool 'aks-agent-pool.bicep' = if (hasAgentPool) { - name: 'aks-node-pool' - params: { - clusterName: managedCluster.outputs.clusterName - name: 'npuserpool' - config: union({ name: 'npuser', mode: 'User' }, nodePoolBase, agentPoolSpec) - } -} - -// Creates container registry (ACR) -module containerRegistry 'container-registry.bicep' = { - name: 'container-registry' - params: { - name: containerRegistryName - location: location - tags: tags - workspaceId: !empty(logAnalyticsName) ? logAnalytics.id : '' - } -} - -// Grant ACR Pull access from cluster managed identity to container registry -module containerRegistryAccess '../security/registry-access.bicep' = { - name: 'cluster-container-registry-access' - params: { - containerRegistryName: containerRegistry.outputs.name - principalId: managedCluster.outputs.clusterIdentity.objectId - } -} - -// Give AKS cluster access to the specified principal -module clusterAccess '../security/aks-managed-cluster-access.bicep' = if (!empty(principalId) && (enableAzureRbac || disableLocalAccounts)) { - name: 'cluster-access' - params: { - clusterName: managedCluster.outputs.clusterName - principalId: principalId - principalType: principalType - } -} - -// Give the AKS Cluster access to KeyVault -module clusterKeyVaultAccess '../security/keyvault-access.bicep' = { - name: 'cluster-keyvault-access' - params: { - keyVaultName: keyVaultName - principalId: managedCluster.outputs.clusterIdentity.objectId - } -} - -// Helpers for node pool configuration -var nodePoolBase = { - osType: 'Linux' - maxPods: 30 - type: 'VirtualMachineScaleSets' - upgradeSettings: { - maxSurge: '33%' - } -} - -var nodePoolPresets = { - CostOptimised: { - vmSize: 'Standard_B4ms' - count: 1 - minCount: 1 - maxCount: 3 - enableAutoScaling: true - availabilityZones: [] - } - Standard: { - vmSize: 'Standard_DS2_v2' - count: 3 - minCount: 3 - maxCount: 5 - enableAutoScaling: true - availabilityZones: [ - '1' - '2' - '3' - ] - } - HighSpec: { - vmSize: 'Standard_D4s_v3' - count: 3 - minCount: 3 - maxCount: 5 - enableAutoScaling: true - availabilityZones: [ - '1' - '2' - '3' - ] - } -} - -// Module outputs -@description('The resource name of the AKS cluster') -output clusterName string = managedCluster.outputs.clusterName - -@description('The AKS cluster identity') -output clusterIdentity object = managedCluster.outputs.clusterIdentity - -@description('The resource name of the ACR') -output containerRegistryName string = containerRegistry.outputs.name - -@description('The login server for the container registry') -output containerRegistryLoginServer string = containerRegistry.outputs.loginServer diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/appservice-appsettings.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/appservice-appsettings.bicep deleted file mode 100644 index f4b22f8..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/appservice-appsettings.bicep +++ /dev/null @@ -1,17 +0,0 @@ -metadata description = 'Updates app settings for an Azure App Service.' -@description('The name of the app service resource within the current resource group scope') -param name string - -@description('The app settings to be applied to the app service') -@secure() -param appSettings object - -resource appService 'Microsoft.Web/sites@2022-03-01' existing = { - name: name -} - -resource settings 'Microsoft.Web/sites/config@2022-03-01' = { - name: 'appsettings' - parent: appService - properties: appSettings -} diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/appservice.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/appservice.bicep deleted file mode 100644 index 06c35a5..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/appservice.bicep +++ /dev/null @@ -1,74 +0,0 @@ -metadata description = 'Creates an Azure App Service in an existing Azure App Service plan.' -param name string -param location string = resourceGroup().location -param tags object = {} - -// Reference Properties -param appServicePlanId string -param keyVaultName string = '' -param managedIdentity bool = !empty(keyVaultName) - -param storageEndpoint string = '' -param cosmosDbEndpoint string = '' -// Runtime Properties -@allowed([ - 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' -]) -param runtimeName string -param runtimeVersion string -param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' - -// Microsoft.Web/sites/config -param userAssignedIdentityId string = '' -param userAssignedIdentityClientId string = '' - -resource appService 'Microsoft.Web/sites@2022-03-01' = { - name: name - location: location - tags: tags - properties: { - serverFarmId: appServicePlanId - siteConfig: { - windowsFxVersion: runtimeNameAndVersion - webSocketsEnabled: true - } - httpsOnly: true - } - - identity: { - type: 'UserAssigned' - userAssignedIdentities: { - '${userAssignedIdentityId}': {} - } - } - - resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = { - name: 'ftp' - properties: { - allow: false - } - } - - resource basicPublishingCredentialsPoliciesScm 'basicPublishingCredentialsPolicies' = { - name: 'scm' - properties: { - allow: false - } - } -} - -// add connectionstring -resource symbolicname 'Microsoft.Web/sites/config@2022-09-01' = { - name: 'appsettings' - kind: 'string' - parent: appService - properties: { - AZURE_COSMOS_DB_NOSQL_ENDPOINT: cosmosDbEndpoint - STORAGE_URL: storageEndpoint - AZURE_CLIENT_ID: userAssignedIdentityClientId - } -} - -output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' -output name string = appService.name -output uri string = 'https://${appService.properties.defaultHostName}' diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/appserviceplan.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/appserviceplan.bicep deleted file mode 100644 index f2947b7..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/appserviceplan.bicep +++ /dev/null @@ -1,19 +0,0 @@ -metadata description = 'Creates an Azure App Service plan.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param kind string = '' -param reserved bool = true -param sku object - -resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { - name: name - location: location - tags: tags - sku: sku - kind: kind -} - -output id string = appServicePlan.id -output name string = appServicePlan.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-app-upsert.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-app-upsert.bicep deleted file mode 100644 index 870e74a..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-app-upsert.bicep +++ /dev/null @@ -1,109 +0,0 @@ -metadata description = 'Creates or updates an existing Azure Container App.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('The environment name for the container apps') -param containerAppsEnvironmentName string - -@description('The number of CPU cores allocated to a single container instance, e.g., 0.5') -param containerCpuCoreCount string = '0.5' - -@description('The maximum number of replicas to run. Must be at least 1.') -@minValue(1) -param containerMaxReplicas int = 10 - -@description('The amount of memory allocated to a single container instance, e.g., 1Gi') -param containerMemory string = '1.0Gi' - -@description('The minimum number of replicas to run. Must be at least 0.') -param containerMinReplicas int = 0 - -@description('The name of the container') -param containerName string = 'main' - -@description('The name of the container registry') -param containerRegistryName string = '' - -@description('Hostname suffix for container registry. Set when deploying to sovereign clouds') -param containerRegistryHostSuffix string = 'azurecr.io' - -@allowed([ 'http', 'grpc' ]) -@description('The protocol used by Dapr to connect to the app, e.g., HTTP or gRPC') -param daprAppProtocol string = 'http' - -@description('Enable or disable Dapr for the container app') -param daprEnabled bool = false - -@description('The Dapr app ID') -param daprAppId string = containerName - -@description('Specifies if the resource already exists') -param exists bool = false - -@description('Specifies if Ingress is enabled for the container app') -param ingressEnabled bool = true - -@description('The type of identity for the resource') -@allowed([ 'None', 'SystemAssigned', 'UserAssigned' ]) -param identityType string = 'None' - -@description('The name of the user-assigned identity') -param identityName string = '' - -@description('The name of the container image') -param imageName string = '' - -@description('The secrets required for the container') -@secure() -param secrets object = {} - -@description('The environment variables for the container') -param env array = [] - -@description('Specifies if the resource ingress is exposed externally') -param external bool = true - -@description('The service binds associated with the container') -param serviceBinds array = [] - -@description('The target port for the container') -param targetPort int = 80 - -resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = if (exists) { - name: name -} - -module app 'container-app.bicep' = { - name: '${deployment().name}-update' - params: { - name: name - location: location - tags: tags - identityType: identityType - identityName: identityName - ingressEnabled: ingressEnabled - containerName: containerName - containerAppsEnvironmentName: containerAppsEnvironmentName - containerRegistryName: containerRegistryName - containerRegistryHostSuffix: containerRegistryHostSuffix - containerCpuCoreCount: containerCpuCoreCount - containerMemory: containerMemory - containerMinReplicas: containerMinReplicas - containerMaxReplicas: containerMaxReplicas - daprEnabled: daprEnabled - daprAppId: daprAppId - daprAppProtocol: daprAppProtocol - secrets: secrets - external: external - env: env - imageName: !empty(imageName) ? imageName : exists ? existingApp.properties.template.containers[0].image : '' - targetPort: targetPort - serviceBinds: serviceBinds - } -} - -output defaultDomain string = app.outputs.defaultDomain -output imageName string = app.outputs.imageName -output name string = app.outputs.name -output uri string = app.outputs.uri diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-app.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-app.bicep deleted file mode 100644 index f1d9284..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-app.bicep +++ /dev/null @@ -1,169 +0,0 @@ -metadata description = 'Creates a container app in an Azure Container App environment.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('Allowed origins') -param allowedOrigins array = [] - -@description('Name of the environment for container apps') -param containerAppsEnvironmentName string - -@description('CPU cores allocated to a single container instance, e.g., 0.5') -param containerCpuCoreCount string = '0.5' - -@description('The maximum number of replicas to run. Must be at least 1.') -@minValue(1) -param containerMaxReplicas int = 10 - -@description('Memory allocated to a single container instance, e.g., 1Gi') -param containerMemory string = '1.0Gi' - -@description('The minimum number of replicas to run. Must be at least 0.') -param containerMinReplicas int = 0 - -@description('The name of the container') -param containerName string = 'main' - -@description('The name of the container registry') -param containerRegistryName string = '' - -@description('Hostname suffix for container registry. Set when deploying to sovereign clouds') -param containerRegistryHostSuffix string = 'azurecr.io' - -@description('The protocol used by Dapr to connect to the app, e.g., http or grpc') -@allowed([ 'http', 'grpc' ]) -param daprAppProtocol string = 'http' - -@description('The Dapr app ID') -param daprAppId string = containerName - -@description('Enable Dapr') -param daprEnabled bool = false - -@description('The environment variables for the container') -param env array = [] - -@description('Specifies if the resource ingress is exposed externally') -param external bool = true - -@description('The name of the user-assigned identity') -param identityName string = '' - -@description('The type of identity for the resource') -@allowed([ 'None', 'SystemAssigned', 'UserAssigned' ]) -param identityType string = 'None' - -@description('The name of the container image') -param imageName string = '' - -@description('Specifies if Ingress is enabled for the container app') -param ingressEnabled bool = true - -param revisionMode string = 'Single' - -@description('The secrets required for the container') -@secure() -param secrets object = {} - -@description('The service binds associated with the container') -param serviceBinds array = [] - -@description('The name of the container apps add-on to use. e.g. redis') -param serviceType string = '' - -@description('The target port for the container') -param targetPort int = 80 - -resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (!empty(identityName)) { - name: identityName -} - -// Private registry support requires both an ACR name and a User Assigned managed identity -var usePrivateRegistry = !empty(identityName) && !empty(containerRegistryName) - -// Automatically set to `UserAssigned` when an `identityName` has been set -var normalizedIdentityType = !empty(identityName) ? 'UserAssigned' : identityType - -module containerRegistryAccess '../security/registry-access.bicep' = if (usePrivateRegistry) { - name: '${deployment().name}-registry-access' - params: { - containerRegistryName: containerRegistryName - principalId: usePrivateRegistry ? userIdentity.properties.principalId : '' - } -} - -resource app 'Microsoft.App/containerApps@2025-01-01' = { - name: name - location: location - tags: tags - // It is critical that the identity is granted ACR pull access before the app is created - // otherwise the container app will throw a provision error - // This also forces us to use an user assigned managed identity since there would no way to - // provide the system assigned identity with the ACR pull access before the app is created - dependsOn: usePrivateRegistry ? [ containerRegistryAccess ] : [] - identity: { - type: normalizedIdentityType - userAssignedIdentities: !empty(identityName) && normalizedIdentityType == 'UserAssigned' ? { '${userIdentity.id}': {} } : null - } - properties: { - managedEnvironmentId: containerAppsEnvironment.id - configuration: { - activeRevisionsMode: revisionMode - ingress: ingressEnabled ? { - external: external - targetPort: targetPort - transport: 'auto' - corsPolicy: { - allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) - } - } : null - dapr: daprEnabled ? { - enabled: true - appId: daprAppId - appProtocol: daprAppProtocol - appPort: ingressEnabled ? targetPort : 0 - } : { enabled: false } - secrets: [for secret in items(secrets): { - name: secret.key - value: secret.value - }] - service: !empty(serviceType) ? { type: serviceType } : null - registries: usePrivateRegistry ? [ - { - server: '${containerRegistryName}.${containerRegistryHostSuffix}' - identity: userIdentity.id - } - ] : [] - } - template: { - serviceBinds: !empty(serviceBinds) ? serviceBinds : null - containers: [ - { - image: !empty(imageName) ? imageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - name: containerName - env: env - resources: { - cpu: json(containerCpuCoreCount) - memory: containerMemory - } - } - ] - scale: { - minReplicas: containerMinReplicas - maxReplicas: containerMaxReplicas - } - } - } -} - -resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' existing = { - name: containerAppsEnvironmentName -} - -output defaultDomain string = containerAppsEnvironment.properties.defaultDomain -output identityPrincipalId string = normalizedIdentityType == 'None' ? '' : (empty(identityName) ? app.identity.principalId : userIdentity.properties.principalId) -output imageName string = imageName -output name string = app.name -output serviceBind object = !empty(serviceType) ? { serviceId: app.id, name: name } : {} -output uri string = ingressEnabled ? 'https://${app.properties.configuration.ingress.fqdn}' : '' diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps-environment.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps-environment.bicep deleted file mode 100644 index d5fc9a6..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps-environment.bicep +++ /dev/null @@ -1,54 +0,0 @@ -metadata description = 'Creates an Azure Container Apps environment.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('Name of the Application Insights resource') -param applicationInsightsName string = '' - -@description('Specifies if Dapr is enabled') -param daprEnabled bool = false - -@description('Name of the Log Analytics workspace') -param logAnalyticsWorkspaceName string = '' - -@description('Subnet resource ID for the Container Apps environment') -param subnetResourceId string = '' - -@description('Whether to use an internal or external load balancer') -@allowed(['Internal', 'External']) -param loadBalancerType string = 'External' - -resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = { - name: name - location: location - tags: tags - properties: { - // appLogsConfiguration: { - // destination: 'log-analytics' - // logAnalyticsConfiguration: { - // customerId: logAnalyticsWorkspace.properties.customerId - // sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey - // } - // } - // daprAIInstrumentationKey: daprEnabled && !empty(applicationInsightsName) ? applicationInsights.properties.InstrumentationKey : '' - - // Add vnet configuration if a subnet is provided - vnetConfiguration: !empty(subnetResourceId) ? { - infrastructureSubnetId: subnetResourceId - internal: loadBalancerType == 'Internal' - } : null - } -} - -// resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { -// name: logAnalyticsWorkspaceName -// } - -// resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (daprEnabled && !empty(applicationInsightsName)) { -// name: applicationInsightsName -// } - -output defaultDomain string = containerAppsEnvironment.properties.defaultDomain -output id string = containerAppsEnvironment.id -output name string = containerAppsEnvironment.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps.bicep deleted file mode 100644 index a7143b0..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps.bicep +++ /dev/null @@ -1,52 +0,0 @@ -metadata description = 'Creates an Azure Container Registry and an Azure Container Apps environment.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param containerAppsEnvironmentName string -param containerRegistryName string -param containerRegistryResourceGroupName string = '' -param containerRegistryAdminUserEnabled bool = false -param logAnalyticsWorkspaceName string = '' -param applicationInsightsName string = '' -param daprEnabled bool = false - -// Virtual network and subnet parameters -param subnetResourceId string = '' -param loadBalancerType string = 'External' - -module containerAppsEnvironment 'container-apps-environment.bicep' = { - name: '${name}-container-apps-environment' - params: { - name: containerAppsEnvironmentName - location: location - tags: tags - logAnalyticsWorkspaceName: logAnalyticsWorkspaceName - applicationInsightsName: applicationInsightsName - daprEnabled: daprEnabled - subnetResourceId: subnetResourceId - loadBalancerType: loadBalancerType - } -} - -module containerRegistry 'container-registry.bicep' = { - name: '${name}-container-registry' - scope: !empty(containerRegistryResourceGroupName) ? resourceGroup(containerRegistryResourceGroupName) : resourceGroup() - params: { - name: containerRegistryName - location: location - adminUserEnabled: containerRegistryAdminUserEnabled - tags: tags - sku: { - name: 'Standard' - } - anonymousPullEnabled: false - } -} - -output defaultDomain string = containerAppsEnvironment.outputs.defaultDomain -output environmentName string = containerAppsEnvironment.outputs.name -output environmentId string = containerAppsEnvironment.outputs.id - -output registryLoginServer string = containerRegistry.outputs.loginServer -output registryName string = containerRegistry.outputs.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps/app.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps/app.bicep deleted file mode 100644 index 8658d07..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps/app.bicep +++ /dev/null @@ -1,123 +0,0 @@ -metadata description = 'Creates an Azure Container Apps app.' - -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('Name of the parent environment for the app.') -param parentEnvironmentName string - -@description('Specifies the docker container image to deploy.') -param containerImage string = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' - -@description('Specifies the container port.') -param targetPort int = 80 - -@description('Number of CPU cores the container can use. Can have a maximum of two decimals.') -param cpuCores string = '0.25' - -@description('Amount of memory (in gibibytes, GiB) allocated to the container up to 4GiB. Can have a maximum of two decimals. Ratio with CPU cores must be equal to 2.') -param memorySize string = '0.5Gi' - -@description('Minimum number of replicas that will be deployed.') -@minValue(1) -@maxValue(25) -param minReplicas int = 1 - -@description('Maximum number of replicas that will be deployed.') -@minValue(1) -param maxReplicas int = 1 - -type envVar = { - name: string - secretRef: string? - value: string? -} - -@description('The environment variables for the container.') -param environmentVariables envVar[] = [] - -type secret = { - name: string - identity: string? - keyVaultUrl: string? - value: string? -} - -@description('The secrets required for the container') -param secrets secret[] = [] - -@description('Specifies if the resource ingress is exposed externally.') -param externalAccess bool = true - -@description('Specifies if Ingress is enabled for the container app.') -param ingressEnabled bool = true - -@description('Allowed CORS origins.') -param allowedOrigins string[] = [] - -type registry = { - server: string - identity: string? - username: string? - passwordSecretRef: string? -} - -@description('List of registries. Defaults to an empty list.') -param registries registry[] = [] - -@description('Enable system-assigned managed identity. Defaults to false.') -param enableSystemAssignedManagedIdentity bool = false - -@description('List of user-assigned managed identities. Defaults to an empty array.') -param userAssignedManagedIdentityIds string[] = [] - -resource environment 'Microsoft.App/managedEnvironments@2023-05-01' existing = { - name: parentEnvironmentName -} - -resource app 'Microsoft.App/containerApps@2023-05-01' = { - name: name - location: location - tags: tags - identity: { - type: enableSystemAssignedManagedIdentity ? !empty(userAssignedManagedIdentityIds) ? 'SystemAssigned,UserAssigned' : 'SystemAssigned' : !empty(userAssignedManagedIdentityIds) ? 'UserAssigned' : 'None' - userAssignedIdentities: !empty(userAssignedManagedIdentityIds) ? toObject(userAssignedManagedIdentityIds, uaid => uaid, uaid => {}) : null - } - properties: { - environmentId: environment.id - configuration: { - ingress: ingressEnabled ? { - external: externalAccess - targetPort: targetPort - transport: 'auto' - corsPolicy: { - allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) - } - } : null - secrets: secrets - registries: !empty(registries) ? registries : null - } - template: { - containers: [ - { - image: containerImage - name: name - resources: { - cpu: json(cpuCores) - memory: memorySize - } - env: environmentVariables - } - ] - scale: { - minReplicas: minReplicas - maxReplicas: maxReplicas - } - } - } -} - -output endpoint string = ingressEnabled ? 'https://${app.properties.configuration.ingress.fqdn}' : '' -output name string = app.name -output systemAssignedManagedIdentityPrincipalId string = enableSystemAssignedManagedIdentity ? app.identity.principalId : '' diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps/managed.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps/managed.bicep deleted file mode 100644 index d997160..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-apps/managed.bicep +++ /dev/null @@ -1,14 +0,0 @@ -metadata description = 'Creates an Azure Container Apps managed environment.' - -param name string -param location string = resourceGroup().location -param tags object = {} - -resource environment 'Microsoft.App/managedEnvironments@2023-05-01' = { - name: name - location: location - tags: tags - properties: {} -} - -output name string = environment.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-registry.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-registry.bicep deleted file mode 100644 index d14731c..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/container-registry.bicep +++ /dev/null @@ -1,137 +0,0 @@ -metadata description = 'Creates an Azure Container Registry.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('Indicates whether admin user is enabled') -param adminUserEnabled bool = false - -@description('Indicates whether anonymous pull is enabled') -param anonymousPullEnabled bool = false - -@description('Azure ad authentication as arm policy settings') -param azureADAuthenticationAsArmPolicy object = { - status: 'enabled' -} - -@description('Indicates whether data endpoint is enabled') -param dataEndpointEnabled bool = false - -@description('Encryption settings') -param encryption object = { - status: 'disabled' -} - -@description('Export policy settings') -param exportPolicy object = { - status: 'enabled' -} - -@description('Metadata search settings') -param metadataSearch string = 'Disabled' - -@description('Options for bypassing network rules') -param networkRuleBypassOptions string = 'AzureServices' - -@description('Public network access setting') -param publicNetworkAccess string = 'Enabled' - -@description('Quarantine policy settings') -param quarantinePolicy object = { - status: 'disabled' -} - -@description('Retention policy settings') -param retentionPolicy object = { - days: 7 - status: 'disabled' -} - -@description('Scope maps setting') -param scopeMaps array = [] - -@description('SKU settings') -param sku object = { - name: 'Basic' -} - -@description('Soft delete policy settings') -param softDeletePolicy object = { - retentionDays: 7 - status: 'disabled' -} - -@description('Trust policy settings') -param trustPolicy object = { - type: 'Notary' - status: 'disabled' -} - -@description('Zone redundancy setting') -param zoneRedundancy string = 'Disabled' - -@description('The log analytics workspace ID used for logging and monitoring') -param workspaceId string = '' - -// 2023-11-01-preview needed for metadataSearch -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = { - name: name - location: location - tags: tags - sku: sku - properties: { - adminUserEnabled: adminUserEnabled - anonymousPullEnabled: anonymousPullEnabled - dataEndpointEnabled: dataEndpointEnabled - encryption: encryption - metadataSearch: metadataSearch - networkRuleBypassOptions: networkRuleBypassOptions - policies:{ - quarantinePolicy: quarantinePolicy - trustPolicy: trustPolicy - retentionPolicy: retentionPolicy - exportPolicy: exportPolicy - azureADAuthenticationAsArmPolicy: azureADAuthenticationAsArmPolicy - softDeletePolicy: softDeletePolicy - } - publicNetworkAccess: publicNetworkAccess - zoneRedundancy: zoneRedundancy - } - - resource scopeMap 'scopeMaps' = [for scopeMap in scopeMaps: { - name: scopeMap.name - properties: scopeMap.properties - }] -} - -// TODO: Update diagnostics to be its own module -// Blocking issue: https://github.com/Azure/bicep/issues/622 -// Unable to pass in a `resource` scope or unable to use string interpolation in resource types -resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { - name: 'registry-diagnostics' - scope: containerRegistry - properties: { - workspaceId: workspaceId - logs: [ - { - category: 'ContainerRegistryRepositoryEvents' - enabled: true - } - { - category: 'ContainerRegistryLoginEvents' - enabled: true - } - ] - metrics: [ - { - category: 'AllMetrics' - enabled: true - timeGrain: 'PT1M' - } - ] - } -} - -output id string = containerRegistry.id -output loginServer string = containerRegistry.properties.loginServer -output name string = containerRegistry.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/functions.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/functions.bicep deleted file mode 100644 index cdf11e9..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/functions.bicep +++ /dev/null @@ -1,100 +0,0 @@ -metadata description = 'Creates an Azure Function in an existing Azure App Service plan.' -param name string -param location string = resourceGroup().location -param tags object = {} - -// Reference Properties -param applicationInsightsName string = '' -param appServicePlanId string -param keyVaultName string = '' -param managedIdentity bool = !empty(keyVaultName) || storageManagedIdentity -param storageAccountName string -param storageManagedIdentity bool = false -param virtualNetworkSubnetId string = '' - -// Runtime Properties -@allowed([ - 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' -]) -param runtimeName string -param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' -param runtimeVersion string - -// Function Settings -@allowed([ - '~4', '~3', '~2', '~1' -]) -param extensionVersion string = '~4' - -// Microsoft.Web/sites Properties -param kind string = 'functionapp,linux' - -// Microsoft.Web/sites/config -param allowedOrigins array = [] -param alwaysOn bool = true -param appCommandLine string = '' -@secure() -param appSettings object = {} -param clientAffinityEnabled bool = false -param enableOryxBuild bool = contains(kind, 'linux') -param functionAppScaleLimit int = -1 -param linuxFxVersion string = runtimeNameAndVersion -param minimumElasticInstanceCount int = -1 -param numberOfWorkers int = -1 -param scmDoBuildDuringDeployment bool = true -param use32BitWorkerProcess bool = false -param healthCheckPath string = '' - -module functions 'appservice.bicep' = { - name: '${name}-functions' - params: { - name: name - location: location - tags: tags - allowedOrigins: allowedOrigins - alwaysOn: alwaysOn - appCommandLine: appCommandLine - applicationInsightsName: applicationInsightsName - appServicePlanId: appServicePlanId - appSettings: union(appSettings, { - AzureWebJobsStorage__accountName: storageManagedIdentity ? storage.name : null - AzureWebJobsStorage: storageManagedIdentity ? null : 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' - FUNCTIONS_EXTENSION_VERSION: extensionVersion - FUNCTIONS_WORKER_RUNTIME: runtimeName - }) - clientAffinityEnabled: clientAffinityEnabled - enableOryxBuild: enableOryxBuild - functionAppScaleLimit: functionAppScaleLimit - healthCheckPath: healthCheckPath - keyVaultName: keyVaultName - kind: kind - linuxFxVersion: linuxFxVersion - managedIdentity: managedIdentity - minimumElasticInstanceCount: minimumElasticInstanceCount - numberOfWorkers: numberOfWorkers - runtimeName: runtimeName - runtimeVersion: runtimeVersion - runtimeNameAndVersion: runtimeNameAndVersion - scmDoBuildDuringDeployment: scmDoBuildDuringDeployment - use32BitWorkerProcess: use32BitWorkerProcess - virtualNetworkSubnetId: virtualNetworkSubnetId - } -} - -module storageOwnerRole '../../core/security/role.bicep' = if (storageManagedIdentity) { - name: 'search-index-contrib-role-api' - params: { - principalId: functions.outputs.identityPrincipalId - // Search Index Data Contributor - roleDefinitionId: '8ebe5a00-799e-43f5-93ac-243d3dce84a7' - principalType: 'ServicePrincipal' - } -} - -resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { - name: storageAccountName -} - -output identityPrincipalId string = managedIdentity ? functions.outputs.identityPrincipalId : '' -output name string = functions.outputs.name -output uri string = functions.outputs.uri diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/host/staticwebapp.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/host/staticwebapp.bicep deleted file mode 100644 index cedaf90..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/host/staticwebapp.bicep +++ /dev/null @@ -1,22 +0,0 @@ -metadata description = 'Creates an Azure Static Web Apps instance.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object = { - name: 'Free' - tier: 'Free' -} - -resource web 'Microsoft.Web/staticSites@2022-03-01' = { - name: name - location: location - tags: tags - sku: sku - properties: { - provider: 'Custom' - } -} - -output name string = web.name -output uri string = 'https://${web.properties.defaultHostname}' diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/monitor/applicationinsights-dashboard.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/monitor/applicationinsights-dashboard.bicep deleted file mode 100644 index d082e66..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/monitor/applicationinsights-dashboard.bicep +++ /dev/null @@ -1,1236 +0,0 @@ -metadata description = 'Creates a dashboard for an Application Insights instance.' -param name string -param applicationInsightsName string -param location string = resourceGroup().location -param tags object = {} - -// 2020-09-01-preview because that is the latest valid version -resource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { - name: name - location: location - tags: tags - properties: { - lenses: [ - { - order: 0 - parts: [ - { - position: { - x: 0 - y: 0 - colSpan: 2 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'id' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/AspNetOverviewPinnedPart' - asset: { - idInputName: 'id' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'overview' - } - } - { - position: { - x: 2 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/ProactiveDetectionAsyncPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'ProactiveDetection' - } - } - { - position: { - x: 3 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'ResourceId' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 4 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - endTime: null - createdTime: '2018-05-04T01:20:33.345Z' - isInitialTime: true - grain: 1 - useDashboardTimeRange: false - } - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 5 - y: 0 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - endTime: null - createdTime: '2018-05-08T18:47:35.237Z' - isInitialTime: true - grain: 1 - useDashboardTimeRange: false - } - } - { - name: 'ConfigurationId' - value: '78ce933e-e864-4b05-a27b-71fd55a6afad' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/AppMapButtonPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 0 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Usage' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 3 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - endTime: null - createdTime: '2018-05-04T01:22:35.782Z' - isInitialTime: true - grain: 1 - useDashboardTimeRange: false - } - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/UsageUsersOverviewPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - } - } - { - position: { - x: 4 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Reliability' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 7 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ResourceId' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - { - name: 'DataModel' - value: { - version: '1.0.0' - timeContext: { - durationMs: 86400000 - createdTime: '2018-05-04T23:42:40.072Z' - isInitialTime: false - grain: 1 - useDashboardTimeRange: false - } - } - isOptional: true - } - { - name: 'ConfigurationId' - value: '8a02f7bf-ac0f-40e1-afe9-f0e72cfee77f' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/CuratedBladeFailuresPinnedPart' - isAdapter: true - asset: { - idInputName: 'ResourceId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'failures' - } - } - { - position: { - x: 8 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Responsiveness\r\n' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 11 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ResourceId' - value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - { - name: 'DataModel' - value: { - version: '1.0.0' - timeContext: { - durationMs: 86400000 - createdTime: '2018-05-04T23:43:37.804Z' - isInitialTime: false - grain: 1 - useDashboardTimeRange: false - } - } - isOptional: true - } - { - name: 'ConfigurationId' - value: '2a8ede4f-2bee-4b9c-aed9-2db0e8a01865' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/CuratedBladePerformancePinnedPart' - isAdapter: true - asset: { - idInputName: 'ResourceId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'performance' - } - } - { - position: { - x: 12 - y: 1 - colSpan: 3 - rowSpan: 1 - } - metadata: { - inputs: [] - type: 'Extension/HubsExtension/PartType/MarkdownPart' - settings: { - content: { - settings: { - content: '# Browser' - title: '' - subtitle: '' - } - } - } - } - } - { - position: { - x: 15 - y: 1 - colSpan: 1 - rowSpan: 1 - } - metadata: { - inputs: [ - { - name: 'ComponentId' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'MetricsExplorerJsonDefinitionId' - value: 'BrowserPerformanceTimelineMetrics' - } - { - name: 'TimeContext' - value: { - durationMs: 86400000 - createdTime: '2018-05-08T12:16:27.534Z' - isInitialTime: false - grain: 1 - useDashboardTimeRange: false - } - } - { - name: 'CurrentFilter' - value: { - eventTypes: [ - 4 - 1 - 3 - 5 - 2 - 6 - 13 - ] - typeFacets: {} - isPermissive: false - } - } - { - name: 'id' - value: { - Name: applicationInsights.name - SubscriptionId: subscription().subscriptionId - ResourceGroup: resourceGroup().name - } - } - { - name: 'Version' - value: '1.0' - } - ] - #disable-next-line BCP036 - type: 'Extension/AppInsightsExtension/PartType/MetricsExplorerBladePinnedPart' - asset: { - idInputName: 'ComponentId' - type: 'ApplicationInsights' - } - defaultMenuItemId: 'browser' - } - } - { - position: { - x: 0 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'sessions/count' - aggregationType: 5 - namespace: 'microsoft.insights/components/kusto' - metricVisualization: { - displayName: 'Sessions' - color: '#47BDF5' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'users/count' - aggregationType: 5 - namespace: 'microsoft.insights/components/kusto' - metricVisualization: { - displayName: 'Users' - color: '#7E58FF' - } - } - ] - title: 'Unique sessions and users' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'segmentationUsers' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 4 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'requests/failed' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Failed requests' - color: '#EC008C' - } - } - ] - title: 'Failed requests' - visualization: { - chartType: 3 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'failures' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 8 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'requests/duration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Server response time' - color: '#00BCF2' - } - } - ] - title: 'Server response time' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'performance' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 12 - y: 2 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/networkDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Page load network connect time' - color: '#7E58FF' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/processingDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Client processing time' - color: '#44F1C8' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/sendDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Send request time' - color: '#EB9371' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'browserTimings/receiveDuration' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Receiving response time' - color: '#0672F1' - } - } - ] - title: 'Average page load time breakdown' - visualization: { - chartType: 3 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 0 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'availabilityResults/availabilityPercentage' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Availability' - color: '#47BDF5' - } - } - ] - title: 'Average availability' - visualization: { - chartType: 3 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - openBladeOnClick: { - openBlade: true - destinationBlade: { - extensionName: 'HubsExtension' - bladeName: 'ResourceMenuBlade' - parameters: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - menuid: 'availability' - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 4 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'exceptions/server' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Server exceptions' - color: '#47BDF5' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'dependencies/failed' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Dependency failures' - color: '#7E58FF' - } - } - ] - title: 'Server exceptions and Dependency failures' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 8 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/processorCpuPercentage' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Processor time' - color: '#47BDF5' - } - } - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/processCpuPercentage' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Process CPU' - color: '#7E58FF' - } - } - ] - title: 'Average processor and process CPU utilization' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 12 - y: 5 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'exceptions/browser' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Browser exceptions' - color: '#47BDF5' - } - } - ] - title: 'Browser exceptions' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 0 - y: 8 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'availabilityResults/count' - aggregationType: 7 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Availability test results count' - color: '#47BDF5' - } - } - ] - title: 'Availability test results count' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 4 - y: 8 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/processIOBytesPerSecond' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Process IO rate' - color: '#47BDF5' - } - } - ] - title: 'Average process I/O rate' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - { - position: { - x: 8 - y: 8 - colSpan: 4 - rowSpan: 3 - } - metadata: { - inputs: [ - { - name: 'options' - value: { - chart: { - metrics: [ - { - resourceMetadata: { - id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' - } - name: 'performanceCounters/memoryAvailableBytes' - aggregationType: 4 - namespace: 'microsoft.insights/components' - metricVisualization: { - displayName: 'Available memory' - color: '#47BDF5' - } - } - ] - title: 'Average available memory' - visualization: { - chartType: 2 - legendVisualization: { - isVisible: true - position: 2 - hideSubtitle: false - } - axisVisualization: { - x: { - isVisible: true - axisType: 2 - } - y: { - isVisible: true - axisType: 1 - } - } - } - } - } - } - { - name: 'sharedTimeRange' - isOptional: true - } - ] - #disable-next-line BCP036 - type: 'Extension/HubsExtension/PartType/MonitorChartPart' - settings: {} - } - } - ] - } - ] - } -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { - name: applicationInsightsName -} diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/monitor/applicationinsights.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/monitor/applicationinsights.bicep deleted file mode 100644 index 850e9fe..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/monitor/applicationinsights.bicep +++ /dev/null @@ -1,31 +0,0 @@ -metadata description = 'Creates an Application Insights instance based on an existing Log Analytics workspace.' -param name string -param dashboardName string = '' -param location string = resourceGroup().location -param tags object = {} -param logAnalyticsWorkspaceId string - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { - name: name - location: location - tags: tags - kind: 'web' - properties: { - Application_Type: 'web' - WorkspaceResourceId: logAnalyticsWorkspaceId - } -} - -module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (!empty(dashboardName)) { - name: 'application-insights-dashboard' - params: { - name: dashboardName - location: location - applicationInsightsName: applicationInsights.name - } -} - -output connectionString string = applicationInsights.properties.ConnectionString -output id string = applicationInsights.id -output instrumentationKey string = applicationInsights.properties.InstrumentationKey -output name string = applicationInsights.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/monitor/loganalytics.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/monitor/loganalytics.bicep deleted file mode 100644 index 33f9dc2..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/monitor/loganalytics.bicep +++ /dev/null @@ -1,22 +0,0 @@ -metadata description = 'Creates a Log Analytics workspace.' -param name string -param location string = resourceGroup().location -param tags object = {} - -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { - name: name - location: location - tags: tags - properties: any({ - retentionInDays: 30 - features: { - searchVersion: 1 - } - sku: { - name: 'PerGB2018' - } - }) -} - -output id string = logAnalytics.id -output name string = logAnalytics.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/monitor/monitoring.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/monitor/monitoring.bicep deleted file mode 100644 index 7476125..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/monitor/monitoring.bicep +++ /dev/null @@ -1,33 +0,0 @@ -metadata description = 'Creates an Application Insights instance and a Log Analytics workspace.' -param logAnalyticsName string -param applicationInsightsName string -param applicationInsightsDashboardName string = '' -param location string = resourceGroup().location -param tags object = {} - -module logAnalytics 'loganalytics.bicep' = { - name: 'loganalytics' - params: { - name: logAnalyticsName - location: location - tags: tags - } -} - -module applicationInsights 'applicationinsights.bicep' = { - name: 'applicationinsights' - params: { - name: applicationInsightsName - location: location - tags: tags - dashboardName: applicationInsightsDashboardName - logAnalyticsWorkspaceId: logAnalytics.outputs.id - } -} - -output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString -output applicationInsightsId string = applicationInsights.outputs.id -output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey -output applicationInsightsName string = applicationInsights.outputs.name -output logAnalyticsWorkspaceId string = logAnalytics.outputs.id -output logAnalyticsWorkspaceName string = logAnalytics.outputs.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/networking/cdn-endpoint.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/networking/cdn-endpoint.bicep deleted file mode 100644 index 5e8ab69..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/networking/cdn-endpoint.bicep +++ /dev/null @@ -1,52 +0,0 @@ -metadata description = 'Adds an endpoint to an Azure CDN profile.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('The name of the CDN profile resource') -@minLength(1) -param cdnProfileName string - -@description('Delivery policy rules') -param deliveryPolicyRules array = [] - -@description('The origin URL for the endpoint') -@minLength(1) -param originUrl string - -resource endpoint 'Microsoft.Cdn/profiles/endpoints@2022-05-01-preview' = { - parent: cdnProfile - name: name - location: location - tags: tags - properties: { - originHostHeader: originUrl - isHttpAllowed: false - isHttpsAllowed: true - queryStringCachingBehavior: 'UseQueryString' - optimizationType: 'GeneralWebDelivery' - origins: [ - { - name: replace(originUrl, '.', '-') - properties: { - hostName: originUrl - originHostHeader: originUrl - priority: 1 - weight: 1000 - enabled: true - } - } - ] - deliveryPolicy: { - rules: deliveryPolicyRules - } - } -} - -resource cdnProfile 'Microsoft.Cdn/profiles@2022-05-01-preview' existing = { - name: cdnProfileName -} - -output id string = endpoint.id -output name string = endpoint.name -output uri string = 'https://${endpoint.properties.hostName}' diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/networking/cdn-profile.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/networking/cdn-profile.bicep deleted file mode 100644 index 27669ee..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/networking/cdn-profile.bicep +++ /dev/null @@ -1,34 +0,0 @@ -metadata description = 'Creates an Azure CDN profile.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@description('The pricing tier of this CDN profile') -@allowed([ - 'Custom_Verizon' - 'Premium_AzureFrontDoor' - 'Premium_Verizon' - 'StandardPlus_955BandWidth_ChinaCdn' - 'StandardPlus_AvgBandWidth_ChinaCdn' - 'StandardPlus_ChinaCdn' - 'Standard_955BandWidth_ChinaCdn' - 'Standard_Akamai' - 'Standard_AvgBandWidth_ChinaCdn' - 'Standard_AzureFrontDoor' - 'Standard_ChinaCdn' - 'Standard_Microsoft' - 'Standard_Verizon' -]) -param sku string = 'Standard_Microsoft' - -resource profile 'Microsoft.Cdn/profiles@2022-05-01-preview' = { - name: name - location: location - tags: tags - sku: { - name: sku - } -} - -output id string = profile.id -output name string = profile.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/networking/cdn.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/networking/cdn.bicep deleted file mode 100644 index de98a1f..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/networking/cdn.bicep +++ /dev/null @@ -1,42 +0,0 @@ -metadata description = 'Creates an Azure CDN profile with a single endpoint.' -param location string = resourceGroup().location -param tags object = {} - -@description('Name of the CDN endpoint resource') -param cdnEndpointName string - -@description('Name of the CDN profile resource') -param cdnProfileName string - -@description('Delivery policy rules') -param deliveryPolicyRules array = [] - -@description('Origin URL for the CDN endpoint') -param originUrl string - -module cdnProfile 'cdn-profile.bicep' = { - name: 'cdn-profile' - params: { - name: cdnProfileName - location: location - tags: tags - } -} - -module cdnEndpoint 'cdn-endpoint.bicep' = { - name: 'cdn-endpoint' - params: { - name: cdnEndpointName - location: location - tags: tags - cdnProfileName: cdnProfile.outputs.name - originUrl: originUrl - deliveryPolicyRules: deliveryPolicyRules - } -} - -output endpointName string = cdnEndpoint.outputs.name -output endpointId string = cdnEndpoint.outputs.id -output profileName string = cdnProfile.outputs.name -output profileId string = cdnProfile.outputs.id -output uri string = cdnEndpoint.outputs.uri diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/networking/vnet.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/networking/vnet.bicep deleted file mode 100644 index e82865d..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/networking/vnet.bicep +++ /dev/null @@ -1,52 +0,0 @@ -// filepath: /Users/nickgreenfield1/workspace/Durable-Task-Scheduler/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/networking/vnet.bicep -@description('The name of the Virtual Network') -param name string - -@description('The Azure region where the Virtual Network should exist') -param location string = resourceGroup().location - -@description('Optional tags for the resources') -param tags object = {} - -@description('The address prefixes of the Virtual Network') -param addressPrefixes array = ['10.0.0.0/16'] - -@description('The subnets to create in the Virtual Network') -param subnets array = [ - { - name: 'infrastructure-subnet' - properties: { - addressPrefix: '10.0.0.0/21' - // Container Apps environments don't need pre-configured delegations - they handle this themselves - delegations: [] - privateEndpointNetworkPolicies: 'Disabled' - privateLinkServiceNetworkPolicies: 'Enabled' - } - } - { - name: 'workload-subnet' - properties: { - addressPrefix: '10.0.8.0/21' - delegations: [] - privateEndpointNetworkPolicies: 'Disabled' - privateLinkServiceNetworkPolicies: 'Enabled' - } - } -] - -resource vnet 'Microsoft.Network/virtualNetworks@2022-07-01' = { - name: name - location: location - tags: tags - properties: { - addressSpace: { - addressPrefixes: addressPrefixes - } - subnets: subnets - } -} - -output id string = vnet.id -output name string = vnet.name -output infrastructureSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', name, 'infrastructure-subnet') -output workloadSubnetId string = resourceId('Microsoft.Network/virtualNetworks/subnets', name, 'workload-subnet') diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/search/search-services.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/search/search-services.bicep deleted file mode 100644 index 33fd83e..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/search/search-services.bicep +++ /dev/null @@ -1,68 +0,0 @@ -metadata description = 'Creates an Azure AI Search instance.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param sku object = { - name: 'standard' -} - -param authOptions object = {} -param disableLocalAuth bool = false -param disabledDataExfiltrationOptions array = [] -param encryptionWithCmk object = { - enforcement: 'Unspecified' -} -@allowed([ - 'default' - 'highDensity' -]) -param hostingMode string = 'default' -param networkRuleSet object = { - bypass: 'None' - ipRules: [] -} -param partitionCount int = 1 -@allowed([ - 'enabled' - 'disabled' -]) -param publicNetworkAccess string = 'enabled' -param replicaCount int = 1 -@allowed([ - 'disabled' - 'free' - 'standard' -]) -param semanticSearch string = 'disabled' - -var searchIdentityProvider = (sku.name == 'free') ? null : { - type: 'SystemAssigned' -} - -resource search 'Microsoft.Search/searchServices@2021-04-01-preview' = { - name: name - location: location - tags: tags - // The free tier does not support managed identity - identity: searchIdentityProvider - properties: { - authOptions: disableLocalAuth ? null : authOptions - disableLocalAuth: disableLocalAuth - disabledDataExfiltrationOptions: disabledDataExfiltrationOptions - encryptionWithCmk: encryptionWithCmk - hostingMode: hostingMode - networkRuleSet: networkRuleSet - partitionCount: partitionCount - publicNetworkAccess: publicNetworkAccess - replicaCount: replicaCount - semanticSearch: semanticSearch - } - sku: sku -} - -output id string = search.id -output endpoint string = 'https://${name}.search.windows.net/' -output name string = search.name -output principalId string = !empty(searchIdentityProvider) ? search.identity.principalId : '' - diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/security/aks-managed-cluster-access.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/security/aks-managed-cluster-access.bicep deleted file mode 100644 index aedb080..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/security/aks-managed-cluster-access.bicep +++ /dev/null @@ -1,27 +0,0 @@ -metadata description = 'Assigns RBAC role to the specified AKS cluster and principal.' - -@description('The AKS cluster name used as the target of the role assignments.') -param clusterName string - -@description('The principal ID to assign the role to.') -param principalId string - -@description('The principal type to assign the role to.') -@allowed(['Device','ForeignGroup','Group','ServicePrincipal','User']) -param principalType string = 'User' - -var aksClusterAdminRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b1ff04bb-8a4e-4dc4-8eb5-8693973ce19b') - -resource aksRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - scope: aksCluster // Use when specifying a scope that is different than the deployment scope - name: guid(subscription().id, resourceGroup().id, principalId, aksClusterAdminRole) - properties: { - roleDefinitionId: aksClusterAdminRole - principalType: principalType - principalId: principalId - } -} - -resource aksCluster 'Microsoft.ContainerService/managedClusters@2023-10-02-preview' existing = { - name: clusterName -} diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/security/configstore-access.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/security/configstore-access.bicep deleted file mode 100644 index de72b94..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/security/configstore-access.bicep +++ /dev/null @@ -1,21 +0,0 @@ -@description('Name of Azure App Configuration store') -param configStoreName string - -@description('The principal ID of the service principal to assign the role to') -param principalId string - -resource configStore 'Microsoft.AppConfiguration/configurationStores@2023-03-01' existing = { - name: configStoreName -} - -var configStoreDataReaderRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '516239f1-63e1-4d78-a4de-a74fb236a071') - -resource configStoreDataReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(subscription().id, resourceGroup().id, principalId, configStoreDataReaderRole) - scope: configStore - properties: { - roleDefinitionId: configStoreDataReaderRole - principalId: principalId - principalType: 'ServicePrincipal' - } -} diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/security/keyvault-access.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/security/keyvault-access.bicep deleted file mode 100644 index 316775f..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/security/keyvault-access.bicep +++ /dev/null @@ -1,22 +0,0 @@ -metadata description = 'Assigns an Azure Key Vault access policy.' -param name string = 'add' - -param keyVaultName string -param permissions object = { secrets: [ 'get', 'list' ] } -param principalId string - -resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { - parent: keyVault - name: name - properties: { - accessPolicies: [ { - objectId: principalId - tenantId: subscription().tenantId - permissions: permissions - } ] - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/security/keyvault-secret.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/security/keyvault-secret.bicep deleted file mode 100644 index 7441b29..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/security/keyvault-secret.bicep +++ /dev/null @@ -1,31 +0,0 @@ -metadata description = 'Creates or updates a secret in an Azure Key Vault.' -param name string -param tags object = {} -param keyVaultName string -param contentType string = 'string' -@description('The value of the secret. Provide only derived values like blob storage access, but do not hard code any secrets in your templates') -@secure() -param secretValue string - -param enabled bool = true -param exp int = 0 -param nbf int = 0 - -resource keyVaultSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { - name: name - tags: tags - parent: keyVault - properties: { - attributes: { - enabled: enabled - exp: exp - nbf: nbf - } - contentType: contentType - value: secretValue - } -} - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { - name: keyVaultName -} diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/security/keyvault.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/security/keyvault.bicep deleted file mode 100644 index c449137..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/security/keyvault.bicep +++ /dev/null @@ -1,34 +0,0 @@ -metadata description = 'Creates an Azure Key Vault.' -param name string -param location string = resourceGroup().location -param tags object = {} - -param principalId string = '' - -@description('Allow the key vault to be used during resource creation.') -param enabledForDeployment bool = false -@description('Allow the key vault to be used for template deployment.') -param enabledForTemplateDeployment bool = false - -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { - name: name - location: location - tags: tags - properties: { - tenantId: subscription().tenantId - sku: { family: 'A', name: 'standard' } - accessPolicies: !empty(principalId) ? [ - { - objectId: principalId - permissions: { secrets: [ 'get', 'list' ] } - tenantId: subscription().tenantId - } - ] : [] - enabledForDeployment: enabledForDeployment - enabledForTemplateDeployment: enabledForTemplateDeployment - } -} - -output endpoint string = keyVault.properties.vaultUri -output id string = keyVault.id -output name string = keyVault.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/security/registry-access.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/security/registry-access.bicep deleted file mode 100644 index fc66837..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/security/registry-access.bicep +++ /dev/null @@ -1,19 +0,0 @@ -metadata description = 'Assigns ACR Pull permissions to access an Azure Container Registry.' -param containerRegistryName string -param principalId string - -var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - -resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - scope: containerRegistry // Use when specifying a scope that is different than the deployment scope - name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole) - properties: { - roleDefinitionId: acrPullRole - principalType: 'ServicePrincipal' - principalId: principalId - } -} - -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { - name: containerRegistryName -} diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/security/role.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/security/role.bicep deleted file mode 100644 index 0b30cfd..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/security/role.bicep +++ /dev/null @@ -1,21 +0,0 @@ -metadata description = 'Creates a role assignment for a service principal.' -param principalId string - -@allowed([ - 'Device' - 'ForeignGroup' - 'Group' - 'ServicePrincipal' - 'User' -]) -param principalType string = 'ServicePrincipal' -param roleDefinitionId string - -resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId) - properties: { - principalId: principalId - principalType: principalType - roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) - } -} diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/storage/storage-account.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/storage/storage-account.bicep deleted file mode 100644 index 8586cf5..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/storage/storage-account.bicep +++ /dev/null @@ -1,102 +0,0 @@ -metadata description = 'Creates an Azure storage account.' -param name string -param location string = resourceGroup().location -param tags object = {} - -@allowed([ - 'Cool' - 'Hot' - 'Premium' ]) -param accessTier string = 'Hot' -param allowBlobPublicAccess bool = false -param allowCrossTenantReplication bool = true -param allowSharedKeyAccess bool = true -param containers array = [] -param corsRules array = [] -param defaultToOAuthAuthentication bool = false -param deleteRetentionPolicy object = {} -@allowed([ 'AzureDnsZone', 'Standard' ]) -param dnsEndpointType string = 'Standard' -param files array = [] -param kind string = 'StorageV2' -param minimumTlsVersion string = 'TLS1_2' -param queues array = [] -param shareDeleteRetentionPolicy object = {} -param supportsHttpsTrafficOnly bool = true -param tables array = [] -param networkAcls object = { - bypass: 'AzureServices' - defaultAction: 'Allow' -} -@allowed([ 'Enabled', 'Disabled' ]) -param publicNetworkAccess string = 'Enabled' -param sku object = { name: 'Standard_LRS' } - -resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = { - name: name - location: location - tags: tags - kind: kind - sku: sku - properties: { - accessTier: accessTier - allowBlobPublicAccess: allowBlobPublicAccess - allowCrossTenantReplication: allowCrossTenantReplication - allowSharedKeyAccess: allowSharedKeyAccess - defaultToOAuthAuthentication: defaultToOAuthAuthentication - dnsEndpointType: dnsEndpointType - minimumTlsVersion: minimumTlsVersion - networkAcls: networkAcls - publicNetworkAccess: publicNetworkAccess - supportsHttpsTrafficOnly: supportsHttpsTrafficOnly - } - - resource blobServices 'blobServices' = if (!empty(containers)) { - name: 'default' - properties: { - cors: { - corsRules: corsRules - } - deleteRetentionPolicy: deleteRetentionPolicy - } - resource container 'containers' = [for container in containers: { - name: container.name - properties: { - publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None' - } - }] - } - - resource fileServices 'fileServices' = if (!empty(files)) { - name: 'default' - properties: { - cors: { - corsRules: corsRules - } - shareDeleteRetentionPolicy: shareDeleteRetentionPolicy - } - } - - resource queueServices 'queueServices' = if (!empty(queues)) { - name: 'default' - properties: { - - } - resource queue 'queues' = [for queue in queues: { - name: queue.name - properties: { - metadata: {} - } - }] - } - - resource tableServices 'tableServices' = if (!empty(tables)) { - name: 'default' - properties: {} - } -} - -output id string = storage.id -output name string = storage.name -output primaryEndpoints object = storage.properties.primaryEndpoints -output blobEndpoint string = storage.properties.primaryEndpoints.blob diff --git a/samples/durable-task-sdks/python/function-chaining/infra/core/testing/loadtesting.bicep b/samples/durable-task-sdks/python/function-chaining/infra/core/testing/loadtesting.bicep deleted file mode 100644 index 4678108..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/core/testing/loadtesting.bicep +++ /dev/null @@ -1,15 +0,0 @@ -param name string -param location string = resourceGroup().location -param managedIdentity bool = false -param tags object = {} - -resource loadTest 'Microsoft.LoadTestService/loadTests@2022-12-01' = { - name: name - location: location - tags: tags - identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } - properties: { - } -} - -output loadTestingName string = loadTest.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/main.bicep b/samples/durable-task-sdks/python/function-chaining/infra/main.bicep deleted file mode 100644 index 3770525..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/main.bicep +++ /dev/null @@ -1,197 +0,0 @@ -targetScope = 'subscription' - -// The main bicep module to provision Azure resources. -// For a more complete walkthrough to understand how this file works with azd, -// see https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-create - -@minLength(1) -@maxLength(64) -@description('Name of the the environment which is used to generate a short unique hash used in all resources.') -param environmentName string - -@minLength(1) -@description('Primary location for all resources') -param location string - -@description('Id of the user or app to assign application roles') -param principalId string = '' - -param containerAppsEnvName string = '' -param containerAppsAppName string = '' -param containerRegistryName string = '' -param dtsLocation string = 'centralus' -param dtsSkuName string = 'Dedicated' -param dtsCapacity int = 1 -param dtsName string = '' -param taskHubName string = '' - -param clientsServiceName string = 'client' -param workerServiceName string = 'worker' - -// Optional parameters to override the default azd resource naming conventions. -// Add the following to main.parameters.json to provide values: -// "resourceGroupName": { -// "value": "myGroupName" -// } -param resourceGroupName string = '' - -var abbrs = loadJsonContent('./abbreviations.json') - -// tags that should be applied to all resources. -var tags = { - // Tag all resources with the environment name. - 'azd-env-name': environmentName -} - -// Generate a unique token to be used in naming resources. -// Remove linter suppression after using. -#disable-next-line no-unused-vars -var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) - -// Organize resources in a resource group -resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { - name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' - location: location - tags: tags -} - -// Add resources to be provisioned below. -// A full example that leverages azd bicep modules can be seen in the todo-python-mongo template: -// https://github.com/Azure-Samples/todo-python-mongo/tree/main/infra - -// Create a user assigned identity -module identity './app/user-assigned-identity.bicep' = { - name: 'identity' - scope: rg - params: { - name: 'dts-ca-identity' - } -} - -module identityAssignDTS './core/security/role.bicep' = { - name: 'identityAssignDTS' - scope: rg - params: { - principalId: identity.outputs.principalId - roleDefinitionId: '0ad04412-c4d5-4796-b79c-f76d14c8d402' - principalType: 'ServicePrincipal' - } -} - -module identityAssignDTSDash './core/security/role.bicep' = { - name: 'identityAssignDTSDash' - scope: rg - params: { - principalId: principalId - roleDefinitionId: '0ad04412-c4d5-4796-b79c-f76d14c8d402' - principalType: 'User' - } -} - -// Create virtual network with subnets for Container Apps -module vnet './core/networking/vnet.bicep' = { - name: 'vnet' - scope: rg - params: { - name: '${abbrs.networkVirtualNetworks}${resourceToken}' - location: location - tags: tags - } -} - -// Container apps env and registry -module containerAppsEnv './core/host/container-apps.bicep' = { - name: 'container-apps' - scope: rg - params: { - name: 'app' - containerAppsEnvironmentName: !empty(containerAppsEnvName) ? containerAppsEnvName : '${abbrs.appManagedEnvironments}${resourceToken}' - containerRegistryName: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' - location: location - // Add subnet configuration - subnetResourceId: vnet.outputs.infrastructureSubnetId - loadBalancerType: 'External' // Can be changed to 'Internal' if needed - } -} - -module dts './app/dts.bicep' = { - scope: rg - name: 'dtsResource' - params: { - name: !empty(dtsName) ? dtsName : '${abbrs.dts}${resourceToken}' - taskhubname: !empty(taskHubName) ? taskHubName : '${abbrs.taskhub}${resourceToken}' - location: dtsLocation - tags: tags - ipAllowlist: [ - '0.0.0.0/0' - ] - skuName: dtsSkuName - skuCapacity: dtsCapacity - } -} - - -// Container app -module client 'app/app.bicep' = { - name: clientsServiceName - scope: rg - params: { - appName: !empty(containerAppsAppName) ? '${containerAppsAppName}-client' : '${abbrs.appContainerApps}${resourceToken}-client' - containerAppsEnvironmentName: containerAppsEnv.outputs.environmentName - containerRegistryName: containerAppsEnv.outputs.registryName - userAssignedManagedIdentity: { - resourceId: identity.outputs.resourceId - clientId: identity.outputs.clientId - } - location: location - tags: tags - serviceName: 'client' - exists: false - identityName: identity.outputs.name - dtsEndpoint: dts.outputs.dts_URL - taskHubName: dts.outputs.TASKHUB_NAME - } -} - -// Container app -module worker 'app/app.bicep' = { - name: workerServiceName - scope: rg - params: { - appName: !empty(containerAppsAppName) ? '${containerAppsAppName}-worker' : '${abbrs.appContainerApps}${resourceToken}-worker' - containerAppsEnvironmentName: containerAppsEnv.outputs.environmentName - containerRegistryName: containerAppsEnv.outputs.registryName - userAssignedManagedIdentity: { - resourceId: identity.outputs.resourceId - clientId: identity.outputs.clientId - } - location: location - tags: tags - serviceName: 'worker' - exists: false - identityName: identity.outputs.name - dtsEndpoint: dts.outputs.dts_URL - taskHubName: dts.outputs.TASKHUB_NAME - } -} - -// Add outputs from the deployment here, if needed. -// -// This allows the outputs to be referenced by other bicep deployments in the deployment pipeline, -// or by the local machine as a way to reference created resources in Azure for local development. -// Secrets should not be added here. -// -// Outputs are automatically saved in the local azd environment .env file. -// To see these outputs, run `azd env get-values`, or `azd env get-values --output json` for json output. -output AZURE_LOCATION string = location -output AZURE_TENANT_ID string = tenant().tenantId -// Container outputs -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerAppsEnv.outputs.registryLoginServer -output AZURE_CONTAINER_REGISTRY_NAME string = containerAppsEnv.outputs.registryName - -// // Application outputs -// output AZURE_CONTAINER_APP_ENDPOINT string = web.outputs.endpoint -// output AZURE_CONTAINER_ENVIRONMENT_NAME string = web.outputs.envName - -// Identity outputs -output AZURE_USER_ASSIGNED_IDENTITY_NAME string = identity.outputs.name diff --git a/samples/durable-task-sdks/python/function-chaining/infra/main.parameters.json b/samples/durable-task-sdks/python/function-chaining/infra/main.parameters.json deleted file mode 100644 index c8d3453..0000000 --- a/samples/durable-task-sdks/python/function-chaining/infra/main.parameters.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "environmentName": { - "value": "${AZURE_ENV_NAME}" - }, - "location": { - "value": "${AZURE_LOCATION}" - }, - "principalId": { - "value": "${AZURE_PRINCIPAL_ID}" - } - } -} diff --git a/samples/durable-task-sdks/python/versioning/Dockerfile.client b/samples/durable-task-sdks/python/versioning/Dockerfile.client new file mode 100644 index 0000000..8e7b363 --- /dev/null +++ b/samples/durable-task-sdks/python/versioning/Dockerfile.client @@ -0,0 +1,13 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Copy requirements first to leverage Docker cache +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy client application code +COPY client.py . + +# Run the client application +CMD ["python", "client.py"] diff --git a/samples/durable-task-sdks/python/versioning/Dockerfile.worker b/samples/durable-task-sdks/python/versioning/Dockerfile.worker new file mode 100644 index 0000000..31c99a2 --- /dev/null +++ b/samples/durable-task-sdks/python/versioning/Dockerfile.worker @@ -0,0 +1,13 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Copy requirements first to leverage Docker cache +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy worker application code +COPY worker.py . + +# Run the worker application +CMD ["python", "worker.py"] diff --git a/samples/durable-task-sdks/python/versioning/README.md b/samples/durable-task-sdks/python/versioning/README.md new file mode 100644 index 0000000..65b547b --- /dev/null +++ b/samples/durable-task-sdks/python/versioning/README.md @@ -0,0 +1,312 @@ +# Orchestration Versioning Pattern + +## Description of the Sample + +This sample demonstrates the Orchestration Versioning pattern with the Azure Durable Task Scheduler using the Python SDK. Versioning allows you to safely evolve orchestration logic while maintaining backward compatibility for in-flight orchestrations. + +In this sample: +1. A versioned orchestration is defined that changes behavior based on its version +2. The client schedules orchestrations with different version strings (1.0.0, 2.0.0, 3.0.0) +3. The orchestration uses `ctx.version` to branch logic based on the version +4. All versions run on the same worker, ensuring backward compatibility + +**Version history in this sample:** +- **v1.0.0**: Basic hello greeting +- **v2.0.0**: Added goodbye greeting +- **v3.0.0**: Added notification after greeting + +This pattern is useful for: +- Safely deploying new orchestration logic without breaking in-flight workflows +- Maintaining multiple versions of business logic simultaneously +- Gradual rollout of new features +- Avoiding non-deterministic errors during orchestration replay + +## Prerequisites + +1. [Python 3.9+](https://www.python.org/downloads/) +2. [Docker](https://www.docker.com/products/docker-desktop/) (for running the emulator) installed +3. [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli) (if using a deployed Durable Task Scheduler) + +## Configuring Durable Task Scheduler + +There are two ways to run this sample locally: + +### Using the Emulator (Recommended) + +The emulator simulates a scheduler and taskhub in a Docker container, making it ideal for development and learning. + +1. Pull the Docker Image for the Emulator: + ```bash + docker pull mcr.microsoft.com/dts/dts-emulator:latest + ``` + +1. Run the Emulator: + ```bash + docker run --name dtsemulator -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest + ``` +Wait a few seconds for the container to be ready. + +Note: The example code automatically uses the default emulator settings (endpoint: http://localhost:8080, taskhub: default). You don't need to set any environment variables. + +### Using a Deployed Scheduler and Taskhub in Azure + +Local development with a deployed scheduler: + +1. Install the durable task scheduler CLI extension: + + ```bash + az upgrade + az extension add --name durabletask --allow-preview true + ``` + +1. Create a resource group in a region where the Durable Task Scheduler is available: + + ```bash + az provider show --namespace Microsoft.DurableTask --query "resourceTypes[?resourceType=='schedulers'].locations | [0]" --out table + ``` + + ```bash + az group create --name my-resource-group --location + ``` +1. Create a durable task scheduler resource: + + ```bash + az durabletask scheduler create \ + --resource-group my-resource-group \ + --name my-scheduler \ + --ip-allowlist '["0.0.0.0/0"]' \ + --sku-name "Dedicated" \ + --sku-capacity 1 \ + --tags "{'myattribute':'myvalue'}" + ``` + +1. Create a task hub within the scheduler resource: + + ```bash + az durabletask taskhub create \ + --resource-group my-resource-group \ + --scheduler-name my-scheduler \ + --name "my-taskhub" + ``` + +1. Grant the current user permission to connect to the `my-taskhub` task hub: + + ```bash + subscriptionId=$(az account show --query "id" -o tsv) + loggedInUser=$(az account show --query "user.name" -o tsv) + + az role assignment create \ + --assignee $loggedInUser \ + --role "Durable Task Data Contributor" \ + --scope "/subscriptions/$subscriptionId/resourceGroups/my-resource-group/providers/Microsoft.DurableTask/schedulers/my-scheduler/taskHubs/my-taskhub" + ``` + +## How to Run the Sample + +Once you have set up either the emulator or deployed scheduler, follow these steps to run the sample: + +1. First, activate your Python virtual environment (if you're using one): + ```bash + python -m venv venv + source venv/bin/activate # On Windows, use: venv\Scripts\activate + ``` + +1. If you're using a deployed scheduler, you need set Environment Variables: + ```bash + export ENDPOINT=$(az durabletask scheduler show \ + --resource-group my-resource-group \ + --name my-scheduler \ + --query "properties.endpoint" \ + --output tsv) + + export TASKHUB="my-taskhub" + ``` + +1. Install the required packages: + ```bash + pip install -r requirements.txt + ``` + +1. Start the worker in a terminal: + ```bash + python worker.py + ``` + You should see output indicating the worker has started and registered the activities and orchestration. + +1. In a new terminal (with the virtual environment activated if applicable), run the client: + > **Note:** Remember to set the environment variables again if you're using a deployed scheduler. + + ```bash + python client.py [name] + ``` + You can optionally provide a name as an argument. If not provided, "World" will be used. + +## Understanding Orchestration Versioning + +### Setting the Version + +When scheduling an orchestration, the client specifies the version: + +```python +instance_id = client.schedule_new_orchestration( + "versioned_orchestration", + input="World", + version="2.0.0" # Version is set here +) +``` + +### Reading the Version in Orchestrations + +Inside the orchestration, use `ctx.version` to read the version: + +```python +def versioned_orchestration(ctx: task.OrchestrationContext, name: str): + orch_version = ctx.version # e.g., "2.0.0" + + # Always run v1 logic + result = yield ctx.call_activity(activity_v1, input=name) + + # Only run v2+ logic + if compare_version(orch_version, "2.0.0") >= 0: + result = yield ctx.call_activity(activity_v2, input=name) + + return result +``` + +### Version Comparison Helper + +The sample includes a helper function for semantic version comparison: + +```python +from packaging import version + +def compare_version(v1: str | None, v2: str) -> int: + """Compare two version strings. + + Returns: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2 + """ + if v1 is None: + return -1 + try: + ver1 = version.parse(v1) + ver2 = version.parse(v2) + if ver1 < ver2: + return -1 + elif ver1 > ver2: + return 1 + return 0 + except Exception: + # Fall back to string comparison + return (v1 > v2) - (v1 < v2) +``` + +### Why Versioning Matters + +Without versioning, changing orchestration logic can cause **non-deterministic errors**: + +1. An orchestration starts with v1 logic +2. You deploy new code with v2 logic (adds new activity) +3. The orchestration replays but hits the new activity code +4. **ERROR**: History doesn't match the new code path + +With versioning: +1. v1 orchestrations continue using v1 code path +2. New orchestrations use v2 code path +3. Both run on the same worker without conflict + +## Deploying with Azure Developer CLI (AZD) + +This sample includes an `azure.yaml` configuration file that allows you to deploy the entire solution to Azure using Azure Developer CLI (AZD). + +> **Note:** This sample uses the shared infrastructure templates located at [`samples/infra/`](../../../infra/). + +### Prerequisites for AZD Deployment + +1. Install [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) +2. Authenticate with Azure: + ```bash + azd auth login + ``` + +### Deployment Steps + +1. Navigate to the Versioning sample directory: + ```bash + cd /path/to/Durable-Task-Scheduler/samples/durable-task-sdks/python/versioning + ``` + +2. Initialize the Azure Developer CLI project (only needed the first time): + ```bash + azd init + ``` + +3. Provision resources and deploy the application: + ```bash + azd up + ``` + +4. After deployment completes, AZD will display URLs for your deployed services. + +5. Monitor your orchestrations using the Azure Portal by navigating to your Durable Task Scheduler resource. + +## Understanding the Output + +When you run the sample, you'll see output from both the worker and client processes: + +### Worker Output +The worker shows: +- Registration of activities and orchestrator +- Log entries when activities are called +- Which activities are called varies by version + +### Client Output +The client shows three orchestrations with different versions: + +``` +=== Orchestration Versioning Demo === +Testing with name: World + +Scheduling orchestration with version 1.0.0: v1 - Basic hello only + Instance ID: abc123... +Scheduling orchestration with version 2.0.0: v2 - Hello + Goodbye + Instance ID: def456... +Scheduling orchestration with version 3.0.0: v3 - Hello + Goodbye + Notification + Instance ID: ghi789... + +Waiting for orchestrations to complete... + +=== Version 1.0.0 (v1 - Basic hello only) === + Status: COMPLETED + Result: {"version": "1.0.0", "results": ["Hello, World!"]} + +=== Version 2.0.0 (v2 - Hello + Goodbye) === + Status: COMPLETED + Result: {"version": "2.0.0", "results": ["Hello, World!", "Goodbye, World!"]} + +=== Version 3.0.0 (v3 - Hello + Goodbye + Notification) === + Status: COMPLETED + Result: {"version": "3.0.0", "results": ["Hello, World!", "Goodbye, World!", "Notification sent: ..."]} + +=== Demo Complete === +Key takeaway: All versions ran using the same worker code! +``` + +## Reviewing the Orchestration in the Durable Task Scheduler Dashboard + +To access the Durable Task Scheduler Dashboard and review your orchestrations: + +### Using the Emulator +1. Navigate to http://localhost:8082 in your web browser +2. Click on the "default" task hub +3. You'll see orchestration instances in the list +4. Click on an instance ID to view the execution details +5. Notice how different versions have different numbers of activity calls + +### Using a Deployed Scheduler +1. Navigate to the Scheduler resource in the Azure portal +2. Go to the Task Hub subresource that you're using +3. Click on the dashboard URL in the top right corner +4. Search for your orchestration instance ID +5. Review the execution details + +The dashboard helps visualize how different versions execute different code paths while using the same orchestration definition. diff --git a/samples/durable-task-sdks/python/versioning/azure.yaml b/samples/durable-task-sdks/python/versioning/azure.yaml new file mode 100644 index 0000000..e3e6993 --- /dev/null +++ b/samples/durable-task-sdks/python/versioning/azure.yaml @@ -0,0 +1,27 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +# This is an example starter azure.yaml file containing several example services in comments below. +# Make changes as needed to describe your application setup. +# To learn more about the azure.yaml file, visit https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/azd-schema + +# Name of the application. +metadata: + template: versioning-python +name: dts-versioning +infra: + path: ../../../infra +services: + client: + project: . + language: python + host: containerapp + apiVersion: 2025-01-01 + docker: + path: ./Dockerfile.client + worker: + project: . + language: python + host: containerapp + apiVersion: 2025-01-01 + docker: + path: ./Dockerfile.worker diff --git a/samples/durable-task-sdks/python/versioning/client.py b/samples/durable-task-sdks/python/versioning/client.py new file mode 100644 index 0000000..3d9e0ca --- /dev/null +++ b/samples/durable-task-sdks/python/versioning/client.py @@ -0,0 +1,131 @@ +import asyncio +import logging +import sys +import os +from azure.identity import DefaultAzureCredential, ManagedIdentityCredential +from durabletask.client import OrchestrationStatus +from durabletask.azuremanaged.client import DurableTaskSchedulerClient + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def main(): + """Main entry point for the client application.""" + logger.info("Starting Orchestration Versioning client...") + + # Get environment variables for taskhub and endpoint with defaults + taskhub_name = os.getenv("TASKHUB", "default") + endpoint = os.getenv("ENDPOINT", "http://localhost:8080") + + print(f"Using taskhub: {taskhub_name}") + print(f"Using endpoint: {endpoint}") + + # Credential handling with better error management + credential = None + if endpoint != "http://localhost:8080": + try: + # Check if we're running in Azure with a managed identity + client_id = os.getenv("AZURE_MANAGED_IDENTITY_CLIENT_ID") + if client_id: + logger.info(f"Using Managed Identity with client ID: {client_id}") + credential = ManagedIdentityCredential(client_id=client_id) + # Test the credential to make sure it works + credential.get_token("https://management.azure.com/.default") + logger.info("Successfully authenticated with Managed Identity") + else: + # Fall back to DefaultAzureCredential only if no client ID is available + logger.info("No client ID found, falling back to DefaultAzureCredential") + credential = DefaultAzureCredential() + except Exception as e: + logger.error(f"Authentication error: {e}") + logger.warning("Continuing without authentication - this may only work with local emulator") + credential = None + + # Create a client using Azure Managed Durable Task + dts_client = DurableTaskSchedulerClient( + host_address=endpoint, + secure_channel=endpoint != "http://localhost:8080", + taskhub=taskhub_name, + token_credential=credential + ) + + # Get name from command line or use default + name = sys.argv[1] if len(sys.argv) > 1 else "World" + + # Define versions to test + versions_to_test = [ + ("1.0.0", "v1 - Basic hello only"), + ("2.0.0", "v2 - Hello + Goodbye"), + ("3.0.0", "v3 - Hello + Goodbye + Notification"), + ] + + try: + logger.info(f"=== Orchestration Versioning Demo ===") + logger.info(f"Testing with name: {name}") + logger.info("") + + instance_ids = [] + + # Schedule orchestrations with different versions + for version, description in versions_to_test: + logger.info(f"Scheduling orchestration with version {version}: {description}") + + instance_id = dts_client.schedule_new_orchestration( + "versioned_orchestration", + input=name, + version=version + ) + + instance_ids.append((version, instance_id, description)) + logger.info(f" Instance ID: {instance_id}") + + logger.info("") + logger.info("Waiting for orchestrations to complete...") + logger.info("") + + # Wait for all orchestrations to complete + for version, instance_id, description in instance_ids: + try: + result = dts_client.wait_for_orchestration_completion( + instance_id, + timeout=60 + ) + + if result: + if result.runtime_status == OrchestrationStatus.COMPLETED: + logger.info(f"=== Version {version} ({description}) ===") + logger.info(f" Status: COMPLETED") + logger.info(f" Result: {result.serialized_output}") + elif result.runtime_status == OrchestrationStatus.FAILED: + logger.error(f"=== Version {version} ===") + logger.error(f" Status: FAILED") + logger.error(f" Error: {result.failure_details}") + else: + logger.warning(f"=== Version {version} ===") + logger.warning(f" Status: {result.runtime_status}") + else: + logger.warning(f"=== Version {version} ===") + logger.warning(f" Did not complete within timeout") + + except Exception as e: + logger.error(f"Error waiting for version {version}: {e}") + + logger.info("") + logger.info("=== Demo Complete ===") + logger.info("Key takeaway: All versions ran using the same worker code!") + logger.info("Version gating in the orchestration ensures backward compatibility.") + + except KeyboardInterrupt: + logger.info("Client stopped by user") + + except Exception as e: + logger.exception(f"Unexpected error: {e}") + + finally: + logger.info("Client shutting down") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/samples/durable-task-sdks/python/versioning/requirements.txt b/samples/durable-task-sdks/python/versioning/requirements.txt new file mode 100644 index 0000000..212ba0f --- /dev/null +++ b/samples/durable-task-sdks/python/versioning/requirements.txt @@ -0,0 +1,3 @@ +durabletask-azuremanaged +azure-identity +packaging diff --git a/samples/durable-task-sdks/python/versioning/worker.py b/samples/durable-task-sdks/python/versioning/worker.py new file mode 100644 index 0000000..7c62266 --- /dev/null +++ b/samples/durable-task-sdks/python/versioning/worker.py @@ -0,0 +1,159 @@ +import asyncio +import logging +import os +from packaging import version +from azure.identity import DefaultAzureCredential, ManagedIdentityCredential +from durabletask import task +from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +# Helper function to compare versions +def compare_version(v1: str | None, v2: str) -> int: + """Compare two version strings. + + Returns: + -1 if v1 < v2 + 0 if v1 == v2 + 1 if v1 > v2 + """ + if v1 is None: + return -1 + try: + ver1 = version.parse(v1) + ver2 = version.parse(v2) + if ver1 < ver2: + return -1 + elif ver1 > ver2: + return 1 + return 0 + except Exception: + # Fall back to string comparison + if v1 < v2: + return -1 + elif v1 > v2: + return 1 + return 0 + + +# Activity functions +def say_hello(ctx: task.ActivityContext, name: str) -> str: + """Activity that returns a greeting.""" + logger.info(f"Activity say_hello called with: {name}") + return f"Hello, {name}!" + + +def say_goodbye(ctx: task.ActivityContext, name: str) -> str: + """Activity added in v2.0.0 that says goodbye.""" + logger.info(f"Activity say_goodbye called with: {name}") + return f"Goodbye, {name}!" + + +def send_notification(ctx: task.ActivityContext, message: str) -> str: + """Activity added in v3.0.0 that sends a notification.""" + logger.info(f"Activity send_notification called with: {message}") + return f"Notification sent: {message}" + + +# Versioned orchestrator function +def versioned_orchestration(ctx: task.OrchestrationContext, name: str): + """Orchestration that demonstrates version-based branching. + + Version history: + - v1.0.0: Basic hello greeting + - v2.0.0: Added goodbye greeting + - v3.0.0: Added notification after greeting + + The orchestration uses ctx.version to determine which code path to execute, + ensuring backward compatibility for in-flight orchestrations. + """ + results = [] + orch_version = ctx.version + + logger.info(f"Running orchestration with version: {orch_version}") + + # v1.0.0+: Basic hello greeting (all versions) + hello_result = yield ctx.call_activity(say_hello, input=name) + results.append(hello_result) + + # v2.0.0+: Added goodbye greeting + if compare_version(orch_version, "2.0.0") >= 0: + goodbye_result = yield ctx.call_activity(say_goodbye, input=name) + results.append(goodbye_result) + + # v3.0.0+: Added notification + if compare_version(orch_version, "3.0.0") >= 0: + notification_message = f"Completed greeting workflow for {name}" + notification_result = yield ctx.call_activity(send_notification, input=notification_message) + results.append(notification_result) + + return { + "version": orch_version, + "results": results + } + + +async def main(): + """Main entry point for the worker process.""" + logger.info("Starting Orchestration Versioning worker...") + + # Get environment variables for taskhub and endpoint with defaults + taskhub_name = os.getenv("TASKHUB", "default") + endpoint = os.getenv("ENDPOINT", "http://localhost:8080") + + print(f"Using taskhub: {taskhub_name}") + print(f"Using endpoint: {endpoint}") + + # Credential handling with better error management + credential = None + if endpoint != "http://localhost:8080": + try: + # Check if we're running in Azure with a managed identity + client_id = os.getenv("AZURE_MANAGED_IDENTITY_CLIENT_ID") + if client_id: + logger.info(f"Using Managed Identity with client ID: {client_id}") + credential = ManagedIdentityCredential(client_id=client_id) + # Test the credential to make sure it works + credential.get_token("https://management.azure.com/.default") + logger.info("Successfully authenticated with Managed Identity") + else: + # Fall back to DefaultAzureCredential only if no client ID is available + logger.info("No client ID found, falling back to DefaultAzureCredential") + credential = DefaultAzureCredential() + except Exception as e: + logger.error(f"Authentication error: {e}") + logger.warning("Continuing without authentication - this may only work with local emulator") + credential = None + + with DurableTaskSchedulerWorker( + host_address=endpoint, + secure_channel=endpoint != "http://localhost:8080", + taskhub=taskhub_name, + token_credential=credential + ) as worker: + + # Register activities and orchestrators + worker.add_activity(say_hello) + worker.add_activity(say_goodbye) + worker.add_activity(send_notification) + worker.add_orchestrator(versioned_orchestration) + + # Start the worker + worker.start() + logger.info("Worker started. Listening for orchestrations...") + + try: + # Keep the worker running + while True: + await asyncio.sleep(1) + except KeyboardInterrupt: + logger.info("Worker shutdown initiated") + + logger.info("Worker stopped") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/abbreviations.json b/samples/infra/abbreviations.json similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/abbreviations.json rename to samples/infra/abbreviations.json diff --git a/samples/durable-task-sdks/java/function-chaining/infra/app/app.bicep b/samples/infra/app/app.bicep similarity index 100% rename from samples/durable-task-sdks/java/function-chaining/infra/app/app.bicep rename to samples/infra/app/app.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/app/dts.bicep b/samples/infra/app/dts.bicep similarity index 75% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/app/dts.bicep rename to samples/infra/app/dts.bicep index c435832..5a08700 100644 --- a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/app/dts.bicep +++ b/samples/infra/app/dts.bicep @@ -6,7 +6,7 @@ param taskhubname string param skuName string param skuCapacity int -resource dts 'Microsoft.DurableTask/schedulers@2024-10-01-preview' = { +resource dts 'Microsoft.DurableTask/schedulers@2025-11-01' = { location: location tags: tags name: name @@ -19,7 +19,7 @@ resource dts 'Microsoft.DurableTask/schedulers@2024-10-01-preview' = { } } -resource taskhub 'Microsoft.DurableTask/schedulers/taskhubs@2024-10-01-preview' = { +resource taskhub 'Microsoft.DurableTask/schedulers/taskhubs@2025-11-01' = { parent: dts name: taskhubname } diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/app/user-assigned-identity.bicep b/samples/infra/app/user-assigned-identity.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/app/user-assigned-identity.bicep rename to samples/infra/app/user-assigned-identity.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/ai/cognitiveservices.bicep b/samples/infra/core/ai/cognitiveservices.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/ai/cognitiveservices.bicep rename to samples/infra/core/ai/cognitiveservices.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/ai/hub-dependencies.bicep b/samples/infra/core/ai/hub-dependencies.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/ai/hub-dependencies.bicep rename to samples/infra/core/ai/hub-dependencies.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/ai/hub.bicep b/samples/infra/core/ai/hub.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/ai/hub.bicep rename to samples/infra/core/ai/hub.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/ai/project.bicep b/samples/infra/core/ai/project.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/ai/project.bicep rename to samples/infra/core/ai/project.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/config/configstore.bicep b/samples/infra/core/config/configstore.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/config/configstore.bicep rename to samples/infra/core/config/configstore.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/cosmos-account.bicep b/samples/infra/core/database/cosmos/cosmos-account.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/cosmos-account.bicep rename to samples/infra/core/database/cosmos/cosmos-account.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep b/samples/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep rename to samples/infra/core/database/cosmos/mongo/cosmos-mongo-account.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep b/samples/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep rename to samples/infra/core/database/cosmos/mongo/cosmos-mongo-db.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/sql/cosmos-sql-account.bicep b/samples/infra/core/database/cosmos/sql/cosmos-sql-account.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/sql/cosmos-sql-account.bicep rename to samples/infra/core/database/cosmos/sql/cosmos-sql-account.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/sql/cosmos-sql-db.bicep b/samples/infra/core/database/cosmos/sql/cosmos-sql-db.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/sql/cosmos-sql-db.bicep rename to samples/infra/core/database/cosmos/sql/cosmos-sql-db.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep b/samples/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep rename to samples/infra/core/database/cosmos/sql/cosmos-sql-role-assign.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep b/samples/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep rename to samples/infra/core/database/cosmos/sql/cosmos-sql-role-def.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/mysql/flexibleserver.bicep b/samples/infra/core/database/mysql/flexibleserver.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/mysql/flexibleserver.bicep rename to samples/infra/core/database/mysql/flexibleserver.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/postgresql/flexibleserver.bicep b/samples/infra/core/database/postgresql/flexibleserver.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/postgresql/flexibleserver.bicep rename to samples/infra/core/database/postgresql/flexibleserver.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/sqlserver/sqlserver.bicep b/samples/infra/core/database/sqlserver/sqlserver.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/database/sqlserver/sqlserver.bicep rename to samples/infra/core/database/sqlserver/sqlserver.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/gateway/apim.bicep b/samples/infra/core/gateway/apim.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/gateway/apim.bicep rename to samples/infra/core/gateway/apim.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/ai-environment.bicep b/samples/infra/core/host/ai-environment.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/ai-environment.bicep rename to samples/infra/core/host/ai-environment.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/aks-agent-pool.bicep b/samples/infra/core/host/aks-agent-pool.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/aks-agent-pool.bicep rename to samples/infra/core/host/aks-agent-pool.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/aks-managed-cluster.bicep b/samples/infra/core/host/aks-managed-cluster.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/aks-managed-cluster.bicep rename to samples/infra/core/host/aks-managed-cluster.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/aks.bicep b/samples/infra/core/host/aks.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/aks.bicep rename to samples/infra/core/host/aks.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/appservice-appsettings.bicep b/samples/infra/core/host/appservice-appsettings.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/appservice-appsettings.bicep rename to samples/infra/core/host/appservice-appsettings.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/appservice.bicep b/samples/infra/core/host/appservice.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/appservice.bicep rename to samples/infra/core/host/appservice.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/appserviceplan.bicep b/samples/infra/core/host/appserviceplan.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/appserviceplan.bicep rename to samples/infra/core/host/appserviceplan.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-app-upsert.bicep b/samples/infra/core/host/container-app-upsert.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-app-upsert.bicep rename to samples/infra/core/host/container-app-upsert.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-app.bicep b/samples/infra/core/host/container-app.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-app.bicep rename to samples/infra/core/host/container-app.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-apps-environment.bicep b/samples/infra/core/host/container-apps-environment.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-apps-environment.bicep rename to samples/infra/core/host/container-apps-environment.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-apps.bicep b/samples/infra/core/host/container-apps.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-apps.bicep rename to samples/infra/core/host/container-apps.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-apps/app.bicep b/samples/infra/core/host/container-apps/app.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-apps/app.bicep rename to samples/infra/core/host/container-apps/app.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-apps/managed.bicep b/samples/infra/core/host/container-apps/managed.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-apps/managed.bicep rename to samples/infra/core/host/container-apps/managed.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-registry.bicep b/samples/infra/core/host/container-registry.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/container-registry.bicep rename to samples/infra/core/host/container-registry.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/functions.bicep b/samples/infra/core/host/functions.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/functions.bicep rename to samples/infra/core/host/functions.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/staticwebapp.bicep b/samples/infra/core/host/staticwebapp.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/host/staticwebapp.bicep rename to samples/infra/core/host/staticwebapp.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/monitor/applicationinsights-dashboard.bicep b/samples/infra/core/monitor/applicationinsights-dashboard.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/monitor/applicationinsights-dashboard.bicep rename to samples/infra/core/monitor/applicationinsights-dashboard.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/monitor/applicationinsights.bicep b/samples/infra/core/monitor/applicationinsights.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/monitor/applicationinsights.bicep rename to samples/infra/core/monitor/applicationinsights.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/monitor/loganalytics.bicep b/samples/infra/core/monitor/loganalytics.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/monitor/loganalytics.bicep rename to samples/infra/core/monitor/loganalytics.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/monitor/monitoring.bicep b/samples/infra/core/monitor/monitoring.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/monitor/monitoring.bicep rename to samples/infra/core/monitor/monitoring.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/networking/cdn-endpoint.bicep b/samples/infra/core/networking/cdn-endpoint.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/networking/cdn-endpoint.bicep rename to samples/infra/core/networking/cdn-endpoint.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/networking/cdn-profile.bicep b/samples/infra/core/networking/cdn-profile.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/networking/cdn-profile.bicep rename to samples/infra/core/networking/cdn-profile.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/networking/cdn.bicep b/samples/infra/core/networking/cdn.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/networking/cdn.bicep rename to samples/infra/core/networking/cdn.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/networking/vnet.bicep b/samples/infra/core/networking/vnet.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/networking/vnet.bicep rename to samples/infra/core/networking/vnet.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/search/search-services.bicep b/samples/infra/core/search/search-services.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/search/search-services.bicep rename to samples/infra/core/search/search-services.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/aks-managed-cluster-access.bicep b/samples/infra/core/security/aks-managed-cluster-access.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/aks-managed-cluster-access.bicep rename to samples/infra/core/security/aks-managed-cluster-access.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/configstore-access.bicep b/samples/infra/core/security/configstore-access.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/configstore-access.bicep rename to samples/infra/core/security/configstore-access.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/keyvault-access.bicep b/samples/infra/core/security/keyvault-access.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/keyvault-access.bicep rename to samples/infra/core/security/keyvault-access.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/keyvault-secret.bicep b/samples/infra/core/security/keyvault-secret.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/keyvault-secret.bicep rename to samples/infra/core/security/keyvault-secret.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/keyvault.bicep b/samples/infra/core/security/keyvault.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/keyvault.bicep rename to samples/infra/core/security/keyvault.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/registry-access.bicep b/samples/infra/core/security/registry-access.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/registry-access.bicep rename to samples/infra/core/security/registry-access.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/role.bicep b/samples/infra/core/security/role.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/security/role.bicep rename to samples/infra/core/security/role.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/storage/storage-account.bicep b/samples/infra/core/storage/storage-account.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/storage/storage-account.bicep rename to samples/infra/core/storage/storage-account.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/testing/loadtesting.bicep b/samples/infra/core/testing/loadtesting.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/core/testing/loadtesting.bicep rename to samples/infra/core/testing/loadtesting.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/main.bicep b/samples/infra/main.bicep similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/main.bicep rename to samples/infra/main.bicep diff --git a/samples/durable-task-sdks/dotnet/FunctionChaining/infra/main.parameters.json b/samples/infra/main.parameters.json similarity index 100% rename from samples/durable-task-sdks/dotnet/FunctionChaining/infra/main.parameters.json rename to samples/infra/main.parameters.json From d5285bf83b35054d71bde8363c59474b8758928d Mon Sep 17 00:00:00 2001 From: Tomer Rosenthal Date: Fri, 30 Jan 2026 12:44:00 -0800 Subject: [PATCH 7/7] feat: Add orchestrator and activity registration to Durable Task Scheduler worker --- .github/skills/durable-task-python/references/setup.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/skills/durable-task-python/references/setup.md b/.github/skills/durable-task-python/references/setup.md index 1b9b469..1ca87b5 100644 --- a/.github/skills/durable-task-python/references/setup.md +++ b/.github/skills/durable-task-python/references/setup.md @@ -450,6 +450,9 @@ with DurableTaskSchedulerWorker( log_handler=log_handler, log_formatter=log_formatter ) as worker: + worker.add_orchestrator(my_orchestration) + worker.add_activity(my_activity) + worker.start() # ... ```