Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
WIP

Show updates in UI from config

Remove calls from resource
  • Loading branch information
davidfowl committed Mar 1, 2024
1 parent ac95db5 commit 2a352e2
Show file tree
Hide file tree
Showing 20 changed files with 462 additions and 151 deletions.
4 changes: 2 additions & 2 deletions playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
builder.AddAzureProvisioning();

var db = builder.AddAzureCosmosDB("cosmos")
.AddDatabase("db")
.RunAsEmulator();
.AddDatabase("db");
// .RunAsEmulator();

builder.AddProject<Projects.CosmosEndToEnd_ApiService>("api")
.WithReference(db);
Expand Down
28 changes: 11 additions & 17 deletions playground/CustomResources/CustomResources.AppHost/TestResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public static IResourceBuilder<TestResource> AddTestResource(this IDistributedAp
builder.Services.AddLifecycleHook<TestResourceLifecycleHook>();

var rb = builder.AddResource(new TestResource(name))
.WithResourceLogger()
.WithResourceUpdates(() => new()
.WithResourceLogging()
.WithResourceUpdates(new()
{
ResourceType = "Test Resource",
State = "Starting",
Expand All @@ -32,45 +32,39 @@ internal sealed class TestResourceLifecycleHook : IDistributedApplicationLifecyc
{
private readonly CancellationTokenSource _tokenSource = new();

public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
public Task AfterResourcesProcessedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
foreach (var item in appModel.Resources.OfType<TestResource>())
{
if (item.TryGetLastAnnotation<ResourceUpdatesAnnotation>(out var resourceUpdates) &&
item.TryGetLastAnnotation<ResourceLoggerAnnotation>(out var loggerAnnotation))
if (item.TryGetLastAnnotation<ResourceLoggerAnnotation>(out var loggerAnnotation) &&
item.TryGetLastAnnotation<ResourceSnapshotAnnotation>(out var snapshotAnnotation))
{
var states = new[] { "Starting", "Running", "Finished" };

Task.Run(async () =>
{
// Simulate custom resource state changes
var state = await resourceUpdates.GetInitialSnapshotAsync(_tokenSource.Token);
var seconds = Random.Shared.Next(2, 12);
state = state with
{
Properties = [.. state.Properties, ("Interval", seconds.ToString(CultureInfo.InvariantCulture))]
};
loggerAnnotation.Logger.LogInformation("Starting test resource {ResourceName} with update interval {Interval} seconds", item.Name, seconds);
// This might run before the dashboard is ready to receive updates, but it will be queued.
await resourceUpdates.UpdateStateAsync(state);
await snapshotAnnotation.PublishUpdateAsync(state => state with
{
Properties = [.. state.Properties, ("Interval", seconds.ToString(CultureInfo.InvariantCulture))]
});
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(seconds));
while (await timer.WaitForNextTickAsync(_tokenSource.Token))
{
var randomState = states[Random.Shared.Next(0, states.Length)];
state = state with
await snapshotAnnotation.PublishUpdateAsync(state => state with
{
State = randomState
};
});
loggerAnnotation.Logger.LogInformation("Test resource {ResourceName} is now in state {State}", item.Name, randomState);
await resourceUpdates.UpdateStateAsync(state);
}
},
cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<li>
@if (displayedEndpoint.Url != null)
{
<a href="@displayedEndpoint.Url" target="_blank">@displayedEndpoint.Url</a>
<a href="@displayedEndpoint.Url" target="_blank">@displayedEndpoint.Text</a>
}
else
{
Expand Down
61 changes: 48 additions & 13 deletions src/Aspire.Dashboard/Model/ResourceEndpointHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,63 @@ internal static class ResourceEndpointHelpers
/// </summary>
public static List<DisplayedEndpoint> GetEndpoints(ILogger logger, ResourceViewModel resource, bool excludeServices = false, bool includeEndpointUrl = false)
{
var isKnownResourceType = resource.IsContainer() || resource.IsExecutable(allowSubtypes: false) || resource.IsProject();

var displayedEndpoints = new List<DisplayedEndpoint>();

if (!excludeServices)
if (isKnownResourceType)
{
foreach (var service in resource.Services)
if (!excludeServices)
{
displayedEndpoints.Add(new DisplayedEndpoint
foreach (var service in resource.Services)
{
Name = service.Name,
Text = service.AddressAndPort,
Address = service.AllocatedAddress,
Port = service.AllocatedPort
});
displayedEndpoints.Add(new DisplayedEndpoint
{
Name = service.Name,
Text = service.AddressAndPort,
Address = service.AllocatedAddress,
Port = service.AllocatedPort
});
}
}
}

foreach (var endpoint in resource.Endpoints)
foreach (var endpoint in resource.Endpoints)
{
ProcessUrl(logger, resource, displayedEndpoints, endpoint.ProxyUrl, "ProxyUrl");
if (includeEndpointUrl)
{
ProcessUrl(logger, resource, displayedEndpoints, endpoint.EndpointUrl, "EndpointUrl");
}
}
}
else
{
ProcessUrl(logger, resource, displayedEndpoints, endpoint.ProxyUrl, "ProxyUrl");
if (includeEndpointUrl)
// Look for services with an address (which might be a URL) and use that to match up with endpoints.
// otherwise, just display the endpoints.
var addressLookup = resource.Services.Where(s => s.AllocatedAddress is not null)
.ToDictionary(s => s.AllocatedAddress!);

foreach (var endpoint in resource.Endpoints)
{
ProcessUrl(logger, resource, displayedEndpoints, endpoint.EndpointUrl, "EndpointUrl");
if (addressLookup.TryGetValue(endpoint.EndpointUrl, out var service))
{
displayedEndpoints.Add(new DisplayedEndpoint
{
Name = service.Name,
Url = endpoint.EndpointUrl,
Text = service.Name,
Address = service.AllocatedAddress,
Port = service.AllocatedPort
});
}
else
{
displayedEndpoints.Add(new DisplayedEndpoint
{
Name = endpoint.EndpointUrl,
Text = endpoint.EndpointUrl
});
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,41 @@ private static IResource PromoteAzureResourceFromAnnotation(IResource resource)
}
}

public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
// TODO: Make this more general purpose
if (executionContext.IsPublishMode)
{
return;
return Task.CompletedTask;
}

var azureResources = appModel.Resources.Select(PromoteAzureResourceFromAnnotation).OfType<IAzureResource>();
if (!azureResources.OfType<IAzureResource>().Any())
{
return Task.CompletedTask;
}

foreach (var r in azureResources)
{
r.ProvisioningTaskCompletionSource = new();

// We're going to be provisioning this resource so we need to add the annotations
var updatesAnnotation = new ResourceUpdatesAnnotation();
r.Annotations.Add(updatesAnnotation);
r.Annotations.Add(new ResourceSnapshotAnnotation(new()
{
State = "Starting",
ResourceType = r.GetType().Name.Replace("Resource", ""),
Properties = [],
},
updatesAnnotation));
}

return Task.CompletedTask;
}

public async Task AfterResourcesProcessedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
var azureResources = appModel.Resources.Select(PromoteAzureResourceFromAnnotation).OfType<IAzureResource>();
if (!azureResources.OfType<IAzureResource>().Any())
{
Expand All @@ -86,15 +113,26 @@ private async Task ProvisionAzureResources(IConfiguration configuration, IHostEn
return new ArmClient(credential, subscriptionId);
});

var subscriptionLazy = new Lazy<Task<SubscriptionResource>>(async () =>
var subscriptionLazy = new Lazy<Task<(SubscriptionResource, TenantResource)>>(async () =>
{
logger.LogInformation("Getting default subscription...");
var value = await armClientLazy.Value.GetDefaultSubscriptionAsync(cancellationToken).ConfigureAwait(false);
logger.LogInformation("Default subscription: {name} ({subscriptionId})", value.Data.DisplayName, value.Id);
return value;
logger.LogInformation("Getting tenant...");
await foreach (var tenant in armClientLazy.Value.GetTenants().GetAllAsync(cancellationToken: cancellationToken).ConfigureAwait(false))
{
if (tenant.Data.TenantId == value.Data.TenantId)
{
logger.LogInformation("Tenant: {tenantId}", tenant.Data.TenantId);
return (value, tenant);
}
}
throw new InvalidOperationException($"Could not find tenant id {value.Data.TenantId} for subscription {value.Data.DisplayName}.");
});

Lazy<Task<(ResourceGroupResource, AzureLocation)>> resourceGroupAndLocationLazy = new(async () =>
Expand All @@ -112,7 +150,7 @@ private async Task ProvisionAzureResources(IConfiguration configuration, IHostEn
string rg => (rg, _options.AllowResourceGroupCreation ?? false)
};
var subscription = await subscriptionLazy.Value.ConfigureAwait(false);
var (subscription, _) = await subscriptionLazy.Value.ConfigureAwait(false);
var resourceGroups = subscription.GetResourceGroups();
ResourceGroupResource? resourceGroup = null;
Expand Down Expand Up @@ -185,6 +223,7 @@ await PopulateExistingAspireResources(

ResourceGroupResource? resourceGroup = null;
SubscriptionResource? subscription = null;
TenantResource? tenant = null;
Dictionary<string, ArmResource>? resourceMap = null;
UserPrincipal? principal = null;
ProvisioningContext? provisioningContext = null;
Expand Down Expand Up @@ -219,26 +258,56 @@ await PopulateExistingAspireResources(

var provisioner = SelectProvisioner(resource);

resource.TryGetLastAnnotation<ResourceLoggerAnnotation>(out var loggerAnnotation);
resource.TryGetLastAnnotation<ResourceSnapshotAnnotation>(out var snapshotAnnotation);

if (provisioner is null)
{
loggerAnnotation?.Logger.LogWarning("No provisioner found for {resourceType} skipping.", resource.GetType().Name);

if (snapshotAnnotation is not null)
{
await snapshotAnnotation.PublishUpdateAsync(state => state with { State = "Running" }).ConfigureAwait(false);
}

logger.LogWarning("No provisioner found for {resourceType} skipping.", resource.GetType().Name);
continue;
}

if (!provisioner.ShouldProvision(configuration, resource))
{
loggerAnnotation?.Logger.LogInformation("Skipping {resourceName} because it is not configured to be provisioned.", resource.Name);

if (snapshotAnnotation is not null)
{
await snapshotAnnotation.PublishUpdateAsync(state => state with { State = "Running" }).ConfigureAwait(false);

}

logger.LogInformation("Skipping {resourceName} because it is not configured to be provisioned.", resource.Name);
continue;
}

if (provisioner.ConfigureResource(configuration, resource))
if (await provisioner.ConfigureResourceAsync(configuration, resource).ConfigureAwait(false))
{
if (snapshotAnnotation is not null)
{
await snapshotAnnotation.PublishUpdateAsync(state => state with { State = "Running" }).ConfigureAwait(false);
}

loggerAnnotation?.Logger.LogInformation("Using connection information stored in user secrets for {resourceName}.", resource.Name);

logger.LogInformation("Using connection information stored in user secrets for {resourceName}.", resource.Name);

continue;
}

subscription ??= await subscriptionLazy.Value.ConfigureAwait(false);
loggerAnnotation?.Logger.LogInformation("Provisioning {resourceName}...", resource.Name);

if (subscription is null || tenant is null)
{
(subscription, tenant) = await subscriptionLazy.Value.ConfigureAwait(false);
}

AzureLocation location = default;

Expand All @@ -249,7 +318,7 @@ await PopulateExistingAspireResources(

resourceMap ??= await resourceMapLazy.Value.ConfigureAwait(false);
principal ??= await principalLazy.Value.ConfigureAwait(false);
provisioningContext ??= new ProvisioningContext(credential, armClientLazy.Value, subscription, resourceGroup, resourceMap, location, principal, userSecrets);
provisioningContext ??= new ProvisioningContext(credential, armClientLazy.Value, subscription, resourceGroup, tenant, resourceMap, location, principal, userSecrets);

var task = provisioner.GetOrCreateResourceAsync(
resource,
Expand All @@ -258,14 +327,19 @@ await PopulateExistingAspireResources(

tasks.Add(task);
}

if (tasks.Count > 0)
{
var task = Task.WhenAll(tasks);

// Suppress throwing so that we can save the user secrets even if the task fails
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);

// Set the completion source for all resources
foreach (var resource in azureResources)
{
resource.ProvisioningTaskCompletionSource?.TrySetResult();
}

// If we created any resources then save the user secrets
if (userSecretsPath is not null)
{
Expand All @@ -279,6 +353,14 @@ await PopulateExistingAspireResources(
// Throw if any of the tasks failed, but after we've saved to user secrets
await task.ConfigureAwait(false);
}
else
{
// Set the completion source for all resources
foreach (var resource in azureResources)
{
resource.ProvisioningTaskCompletionSource?.TrySetResult();
}
}

// Do this in the background to avoid blocking startup
_ = Task.Run(async () =>
Expand Down
Loading

0 comments on commit 2a352e2

Please sign in to comment.