Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updating workflow collection to allow for use of API Token validation #1141

Merged
merged 9 commits into from
Sep 7, 2023
4 changes: 4 additions & 0 deletions examples/Workflow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and
- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/)
- [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/)


## Optional Setup
Dapr workflow, as well as this example program, now support authentication through the use of API tokens. For more information on this, view the following document: [API Token](https://github.com/dapr/dotnet-sdk/docs/api-token.md)

## Projects in sample

This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) .NET project.
Expand Down
229 changes: 120 additions & 109 deletions examples/Workflow/WorkflowConsoleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,16 @@
using var host = builder.Build();
host.Start();

using var daprClient = new DaprClientBuilder().Build();
DaprClient daprClient;
string apiToken = Environment.GetEnvironmentVariable("DAPR_API_TOKEN");
if (!string.IsNullOrEmpty(apiToken))
{
daprClient = new DaprClientBuilder().UseDaprApiToken(apiToken).Build();
}
else
{
daprClient = new DaprClientBuilder().Build();
}
Comment on lines +50 to +59
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need the using. After the initialization, you can just wrap the while in this:

using (daprClient)
{
    // Loop here.
}


// Wait for the sidecar to become available
while (!await daprClient.CheckHealthAsync())
Expand All @@ -70,136 +79,138 @@
await RestockInventory(daprClient, baseInventory);

// Start the input loop
while (true)
using (daprClient)
{
// Get the name of the item to order and make sure we have inventory
string items = string.Join(", ", baseInventory.Select(i => i.Name));
Console.WriteLine($"Enter the name of one of the following items to order [{items}].");
Console.WriteLine("To restock items, type 'restock'.");
string itemName = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(itemName))
{
continue;
}
else if (string.Equals("restock", itemName, StringComparison.OrdinalIgnoreCase))
while (true)
{
await RestockInventory(daprClient, baseInventory);
continue;
}
// Get the name of the item to order and make sure we have inventory
string items = string.Join(", ", baseInventory.Select(i => i.Name));
Console.WriteLine($"Enter the name of one of the following items to order [{items}].");
Console.WriteLine("To restock items, type 'restock'.");
string itemName = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(itemName))
{
continue;
}
else if (string.Equals("restock", itemName, StringComparison.OrdinalIgnoreCase))
{
await RestockInventory(daprClient, baseInventory);
continue;
}

InventoryItem item = baseInventory.FirstOrDefault(item => string.Equals(item.Name, itemName, StringComparison.OrdinalIgnoreCase));
if (item == null)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"We don't have {itemName}!");
Console.ResetColor();
continue;
}
InventoryItem item = baseInventory.FirstOrDefault(item => string.Equals(item.Name, itemName, StringComparison.OrdinalIgnoreCase));
if (item == null)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"We don't have {itemName}!");
Console.ResetColor();
continue;
}

Console.WriteLine($"How many {itemName} would you like to purchase?");
string amountStr = Console.ReadLine().Trim();
if (!int.TryParse(amountStr, out int amount) || amount <= 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Invalid input. Assuming you meant to type '1'.");
Console.ResetColor();
amount = 1;
}
Console.WriteLine($"How many {itemName} would you like to purchase?");
string amountStr = Console.ReadLine().Trim();
if (!int.TryParse(amountStr, out int amount) || amount <= 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Invalid input. Assuming you meant to type '1'.");
Console.ResetColor();
amount = 1;
}

// Construct the order with a unique order ID
string orderId = $"{itemName.ToLowerInvariant()}-{Guid.NewGuid().ToString()[..8]}";
double totalCost = amount * item.PerItemCost;
var orderInfo = new OrderPayload(itemName.ToLowerInvariant(), totalCost, amount);
// Construct the order with a unique order ID
string orderId = $"{itemName.ToLowerInvariant()}-{Guid.NewGuid().ToString()[..8]}";
double totalCost = amount * item.PerItemCost;
var orderInfo = new OrderPayload(itemName.ToLowerInvariant(), totalCost, amount);

// Start the workflow using the order ID as the workflow ID
Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}");
await daprClient.StartWorkflowAsync(
workflowComponent: DaprWorkflowComponent,
workflowName: nameof(OrderProcessingWorkflow),
input: orderInfo,
instanceId: orderId);
// Start the workflow using the order ID as the workflow ID
Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}");
await daprClient.StartWorkflowAsync(
workflowComponent: DaprWorkflowComponent,
workflowName: nameof(OrderProcessingWorkflow),
input: orderInfo,
instanceId: orderId);

// Wait for the workflow to start and confirm the input
GetWorkflowResponse state = await daprClient.WaitForWorkflowStartAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent);
// Wait for the workflow to start and confirm the input
GetWorkflowResponse state = await daprClient.WaitForWorkflowStartAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent);

Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) started successfully with {state.ReadInputAs<OrderPayload>()}");
Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) started successfully with {state.ReadInputAs<OrderPayload>()}");

// Wait for the workflow to complete
while (true)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
// Wait for the workflow to complete
while (true)
{
state = await daprClient.WaitForWorkflowCompletionAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent,
cancellationToken: cts.Token);
break;
}
catch (OperationCanceledException)
{
// Check to see if the workflow is blocked waiting for an approval
state = await daprClient.GetWorkflowAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent);
if (state.Properties.TryGetValue("dapr.workflow.custom_status", out string customStatus) &&
customStatus.Contains("Waiting for approval"))
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) requires approval. Approve? [Y/N]");
string approval = Console.ReadLine();
ApprovalResult approvalResult = ApprovalResult.Unspecified;
if (string.Equals(approval, "Y", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Approving order...");
approvalResult = ApprovalResult.Approved;
}
else if (string.Equals(approval, "N", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Rejecting order...");
approvalResult = ApprovalResult.Rejected;
}

if (approvalResult != ApprovalResult.Unspecified)
state = await daprClient.WaitForWorkflowCompletionAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent,
cancellationToken: cts.Token);
break;
}
catch (OperationCanceledException)
{
// Check to see if the workflow is blocked waiting for an approval
state = await daprClient.GetWorkflowAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent);
if (state.Properties.TryGetValue("dapr.workflow.custom_status", out string customStatus) &&
customStatus.Contains("Waiting for approval"))
{
// Raise the workflow event to the workflow
await daprClient.RaiseWorkflowEventAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent,
eventName: "ManagerApproval",
eventData: approvalResult);
Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) requires approval. Approve? [Y/N]");
string approval = Console.ReadLine();
ApprovalResult approvalResult = ApprovalResult.Unspecified;
if (string.Equals(approval, "Y", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Approving order...");
approvalResult = ApprovalResult.Approved;
}
else if (string.Equals(approval, "N", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Rejecting order...");
approvalResult = ApprovalResult.Rejected;
}

if (approvalResult != ApprovalResult.Unspecified)
{
// Raise the workflow event to the workflow
await daprClient.RaiseWorkflowEventAsync(
instanceId: orderId,
workflowComponent: DaprWorkflowComponent,
eventName: "ManagerApproval",
eventData: approvalResult);
}

// otherwise, keep waiting
}

// otherwise, keep waiting
}
}
}

if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed)
{
OrderResult result = state.ReadOutputAs<OrderResult>();
if (result.Processed)
if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully ({result}).");
Console.ResetColor();
OrderResult result = state.ReadOutputAs<OrderResult>();
if (result.Processed)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully ({result}).");
Console.ResetColor();
}
else
{
Console.WriteLine($"Order workflow is {state.RuntimeStatus} but the order was not processed.");
}
}
else
else if (state.RuntimeStatus == WorkflowRuntimeStatus.Failed)
{
Console.WriteLine($"Order workflow is {state.RuntimeStatus} but the order was not processed.");
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"The workflow failed - {state.FailureDetails}");
Console.ResetColor();
}
}
else if (state.RuntimeStatus == WorkflowRuntimeStatus.Failed)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"The workflow failed - {state.FailureDetails}");
Console.ResetColor();
}

Console.WriteLine();
Console.WriteLine();
}
}

static async Task RestockInventory(DaprClient daprClient, List<InventoryItem> inventory)
{
Console.WriteLine("*** Restocking inventory...");
Expand Down
4 changes: 4 additions & 0 deletions src/Dapr.Workflow/Dapr.Workflow.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<PackageReference Include="Microsoft.DurableTask.Worker.Grpc" Version="1.0.*" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Shared\DaprDefaults.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Dapr.Client\Dapr.Client.csproj" />
<ProjectReference Include="..\Dapr.AspNetCore\Dapr.AspNetCore.csproj" />
Expand Down
63 changes: 58 additions & 5 deletions src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@
namespace Dapr.Workflow
{
using System;
using Grpc.Net.Client;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Net.Http;
using Dapr;

/// <summary>
/// Contains extension methods for using Dapr Workflow with dependency injection.
/// </summary>
public static class WorkflowServiceCollectionExtensions
{

/// <summary>
/// Adds Dapr Workflow support to the service collection.
/// </summary>
Expand Down Expand Up @@ -57,7 +61,18 @@ public static IServiceCollection AddDaprWorkflow(

if (TryGetGrpcAddress(out string address))
{
builder.UseGrpc(address);
var daprApiToken = DaprDefaults.GetDefaultDaprApiToken();
if (!string.IsNullOrEmpty(daprApiToken))
{
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken);
builder.UseGrpc(CreateChannel(address, client));
}
else
{
builder.UseGrpc(address);
}

}
else
{
Expand Down Expand Up @@ -85,7 +100,18 @@ public static IServiceCollection AddDaprWorkflowClient(this IServiceCollection s
{
if (TryGetGrpcAddress(out string address))
{
builder.UseGrpc(address);
var daprApiToken = DaprDefaults.GetDefaultDaprApiToken();
if (!string.IsNullOrEmpty(daprApiToken))
{
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken);
builder.UseGrpc(CreateChannel(address, client));
}
else
{
builder.UseGrpc(address);
}

}
else
{
Expand All @@ -104,13 +130,13 @@ static bool TryGetGrpcAddress(out string address)
// 1. DaprDefaults.cs uses 127.0.0.1 instead of localhost, which prevents testing with Dapr on WSL2 and the app on Windows
// 2. DaprDefaults.cs doesn't compile when the project has C# nullable reference types enabled.
// If the above issues are fixed (ensuring we don't regress anything) we should switch to using the logic in DaprDefaults.cs.
string? daprEndpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT");
var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint();
if (!String.IsNullOrEmpty(daprEndpoint)) {
address = daprEndpoint;
return true;
}

string? daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT");
var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT");
if (int.TryParse(daprPortStr, out int daprGrpcPort))
{
// There is a bug in the Durable Task SDK that requires us to change the format of the address
Expand All @@ -126,6 +152,33 @@ static bool TryGetGrpcAddress(out string address)
address = string.Empty;
return false;
}

static GrpcChannel CreateChannel(string address, HttpClient client)
{

GrpcChannelOptions options = new() { HttpClient = client};
var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint();
if (!String.IsNullOrEmpty(daprEndpoint)) {
return GrpcChannel.ForAddress(daprEndpoint, options);
}

var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT");
if (int.TryParse(daprPortStr, out int daprGrpcPort))
{
// If there is no address passed in, we default to localhost
if (String.IsNullOrEmpty(address))
{
// There is a bug in the Durable Task SDK that requires us to change the format of the address
// depending on the version of .NET that we're targeting. For now, we work around this manually.
#if NET6_0_OR_GREATER
address = $"http://localhost:{daprGrpcPort}";
#else
address = $"localhost:{daprGrpcPort}";
#endif
}

}
return GrpcChannel.ForAddress(address, options);
}
}
}

Loading
Loading