Skip to content

Commit

Permalink
Initial change
Browse files Browse the repository at this point in the history
  • Loading branch information
eerhardt committed Jan 30, 2025
1 parent c3dc761 commit c7745e6
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 24 deletions.
8 changes: 4 additions & 4 deletions playground/Redis/Redis.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
var builder = DistributedApplication.CreateBuilder(args);

var redis = builder.AddRedis("redis")
.WithDataVolume()
.WithRedisCommander(c => c.WithHostPort(33803))
.WithRedisInsight(c => c.WithHostPort(41567));
var redis = builder.AddRedis("redis");
redis.WithDataVolume()
.WithRedisCommander(c => c.WithHostPort(33803).WithParentRelationship(redis.Resource))
.WithRedisInsight(c => c.WithHostPort(41567).WithParentRelationship(redis.Resource));

var garnet = builder.AddGarnet("garnet")
.WithDataVolume();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,6 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
return;
}

static IResource? SelectParentResource(IResource resource) => resource switch
{
IAzureResource ar => ar,
IResourceWithParent rp => SelectParentResource(rp.Parent),
_ => null
};

// Create a map of parents to their children used to propagate state changes later.
_parentChildLookup = appModel.Resources.OfType<IResourceWithParent>().ToLookup(r => r.Parent);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ await notificationService.PublishUpdateAsync(resource, state => state with
{
ResourceType = resource.GetType().Name,
State = new("Starting", KnownResourceStateStyles.Info),
Properties = [
Properties = state.Properties.SetResourcePropertyRange([
new("azure.subscription.id", context.Subscription.Id.Name),
new("azure.resource.group", context.ResourceGroup.Id.Name),
new("azure.tenant.domain", context.Tenant.Data.DefaultDomain),
new("azure.location", context.Location.ToString()),
]
])
}).ConfigureAwait(false);

var resourceLogger = loggerService.GetLogger(resource);
Expand Down
12 changes: 6 additions & 6 deletions src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ public CustomResourceSnapshot ToSnapshot(Container container, CustomResourceSnap
State = state,
// Map a container exit code of -1 (unknown) to null
ExitCode = container.Status?.ExitCode is null or Conventions.UnknownExitCode ? null : container.Status.ExitCode,
Properties = [
Properties = previous.Properties.SetResourcePropertyRange([
new(KnownProperties.Container.Image, container.Spec.Image),
new(KnownProperties.Container.Id, containerId),
new(KnownProperties.Container.Command, container.Spec.Command),
new(KnownProperties.Container.Args, container.Status?.EffectiveArgs ?? []) { IsSensitive = true },
new(KnownProperties.Container.Ports, GetPorts()),
new(KnownProperties.Container.Lifetime, GetContainerLifetime()),
],
]),
EnvironmentVariables = environment,
CreationTimeStamp = container.Metadata.CreationTimestamp?.ToUniversalTime(),
StartTimeStamp = container.Status?.StartupTimestamp?.ToUniversalTime(),
Expand Down Expand Up @@ -111,13 +111,13 @@ public CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceSn
ResourceType = KnownResourceTypes.Project,
State = state,
ExitCode = executable.Status?.ExitCode,
Properties = [
Properties = previous.Properties.SetResourcePropertyRange([
new(KnownProperties.Executable.Path, executable.Spec.ExecutablePath),
new(KnownProperties.Executable.WorkDir, executable.Spec.WorkingDirectory),
new(KnownProperties.Executable.Args, executable.Status?.EffectiveArgs ?? []) { IsSensitive = true },
new(KnownProperties.Executable.Pid, executable.Status?.ProcessId),
new(KnownProperties.Project.Path, projectPath)
],
]),
EnvironmentVariables = environment,
CreationTimeStamp = executable.Metadata.CreationTimestamp?.ToUniversalTime(),
StartTimeStamp = executable.Status?.StartupTimestamp?.ToUniversalTime(),
Expand All @@ -132,12 +132,12 @@ public CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceSn
ResourceType = KnownResourceTypes.Executable,
State = state,
ExitCode = executable.Status?.ExitCode,
Properties = [
Properties = previous.Properties.SetResourcePropertyRange([
new(KnownProperties.Executable.Path, executable.Spec.ExecutablePath),
new(KnownProperties.Executable.WorkDir, executable.Spec.WorkingDirectory),
new(KnownProperties.Executable.Args, executable.Status?.EffectiveArgs ?? []) { IsSensitive = true },
new(KnownProperties.Executable.Pid, executable.Status?.ProcessId)
],
]),
EnvironmentVariables = environment,
CreationTimeStamp = executable.Metadata.CreationTimestamp?.ToUniversalTime(),
StartTimeStamp = executable.Status?.StartupTimestamp?.ToUniversalTime(),
Expand Down
106 changes: 101 additions & 5 deletions src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Immutable;
using System.Data;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Aspire.Dashboard.Model;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dcp;
Expand All @@ -16,7 +17,7 @@ internal sealed class ApplicationOrchestrator
{
private readonly IDcpExecutor _dcpExecutor;
private readonly DistributedApplicationModel _model;
private readonly ILookup<IResource?, IResourceWithParent> _parentChildLookup;
private readonly ILookup<IResource, IResource> _parentChildLookup;
private readonly IDistributedApplicationLifecycleHook[] _lifecycleHooks;
private readonly ResourceNotificationService _notificationService;
private readonly IDistributedApplicationEventing _eventing;
Expand Down Expand Up @@ -107,6 +108,8 @@ await _notificationService.PublishUpdateAsync(context.Resource, s => s with
HealthReports = GetInitialHealthReports(context.Resource)
})
.ConfigureAwait(false);

await SetExecutableChildResourceAsync(context.Resource).ConfigureAwait(false);
break;
case KnownResourceTypes.Container:
await _notificationService.PublishUpdateAsync(context.Resource, s => s with
Expand Down Expand Up @@ -217,7 +220,7 @@ public async Task StopResourceAsync(string resourceName, CancellationToken cance
await _dcpExecutor.StopResourceAsync(resourceReference, cancellationToken).ConfigureAwait(false);
}

private static ILookup<IResource?, IResourceWithParent> GetParentChildLookup(DistributedApplicationModel model)
private static ILookup<IResource, IResource> GetParentChildLookup(DistributedApplicationModel model)
{
static IResource? SelectParentContainerResource(IResource resource) => resource switch
{
Expand All @@ -227,10 +230,88 @@ IResource r when r.IsContainer() => r,
};

// parent -> children lookup
// Built from IResourceWithParent first, then from annotations.
return model.Resources.OfType<IResourceWithParent>()
.Select(x => (Child: x, Root: SelectParentContainerResource(x.Parent)))
.Select(x => (Child: (IResource)x, Root: SelectParentContainerResource(x.Parent)))
.Where(x => x.Root is not null)
.ToLookup(x => x.Root, x => x.Child);
.Concat(GetParentChildRelationshipsFromAnnotations(model))
.ToLookup(x => x.Root!, x => x.Child);
}

private static IEnumerable<(IResource Child, IResource? Root)> GetParentChildRelationshipsFromAnnotations(DistributedApplicationModel model)
{
static bool TryGetParent(IResource resource, [NotNullWhen(true)] out IResource? parent)
{
if (resource.TryGetAnnotationsOfType<ResourceRelationshipAnnotation>(out var relations) &&
relations.LastOrDefault(r => r.Type == KnownRelationshipTypes.Parent) is { } parentRelationship)
{
parent = parentRelationship.Resource;
return true;
}

parent = default;
return false;
}

static IResource? SelectRootResource(IResource? resource) => resource switch
{
IResource r when TryGetParent(r, out var parent) => SelectRootResource(parent) ?? parent,
_ => null
};

var result = model.Resources.Select(x => (Child: x, Root: SelectRootResource(x)))
.Where(x => x.Root is not null)
.ToArray();

ValidateRelationships(result!);

return result;
}

private static void ValidateRelationships((IResource Child, IResource Root)[] relationships)
{
if (relationships.Length == 0)
{
return;
}

// ensure each child only appears once (i.e. doesn't have multiple parents)
List<IResource>? duplicates = null;
var childToParentLookup = new Dictionary<IResource, IResource>();
foreach (var relation in relationships)
{
if (!childToParentLookup.TryAdd(relation.Child, relation.Root))
{
duplicates ??= [];
duplicates.Add(relation.Child);
}
}

if (duplicates is not null)
{
throw new InvalidOperationException($"The following resources have multiple parent relationships: {string.Join(", ", duplicates)}");
}

// ensure no circular dependencies
var visited = new Stack<IResource>();
foreach (var relation in relationships)
{
ValidateNoCircularDependencies(childToParentLookup, relation.Child, visited);
}

static void ValidateNoCircularDependencies(Dictionary<IResource, IResource> childToParentLookup, IResource child, Stack<IResource> visited)
{
visited.Push(child);
if (childToParentLookup.TryGetValue(child, out var parent))
{
if (visited.Contains(parent))
{
throw new InvalidOperationException($"Circular dependency detected: {string.Join(" -> ", visited)} -> {parent}");
}
ValidateNoCircularDependencies(childToParentLookup, parent, visited);
}
visited.Pop();
}
}

private async Task SetChildResourceAsync(IResource resource, string parentName, string? state, DateTime? startTimeStamp, DateTime? stopTimeStamp)
Expand All @@ -247,6 +328,21 @@ await _notificationService.PublishUpdateAsync(child, s => s with
}
}

private async Task SetExecutableChildResourceAsync(IResource resource)
{
// the parent name needs to be an instance name, not the resource name.
// parent the children under the first resource instance.
var parentName = resource.GetResolvedResourceNames()[0];

foreach (var child in _parentChildLookup[resource])
{
await _notificationService.PublishUpdateAsync(child, s => s with
{
Properties = s.Properties.SetResourceProperty(KnownProperties.Resource.ParentName, parentName)
}).ConfigureAwait(false);
}
}

private async Task PublishResourcesWithInitialStateAsync()
{
// Publish the initial state of the resources that have a snapshot annotation.
Expand Down Expand Up @@ -286,7 +382,7 @@ private async Task PublishConnectionStringAvailableEvent(IResource resource, Can
// we need to dispatch the event for the children.
if (_parentChildLookup[resource] is { } children)
{
foreach (var child in children.OfType<IResourceWithConnectionString>())
foreach (var child in children.OfType<IResourceWithConnectionString>().Where(c => c is IResourceWithParent))
{
var childConnectionStringAvailableEvent = new ConnectionStringAvailableEvent(child, _serviceProvider);
await _eventing.PublishAsync(childConnectionStringAvailableEvent, cancellationToken).ConfigureAwait(false);
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ static Aspire.Hosting.ResourceBuilderExtensions.WithCommand<T>(this Aspire.Hosti
static Aspire.Hosting.ResourceBuilderExtensions.WithHealthCheck<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T>! builder, string! key) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T>!
static Aspire.Hosting.ResourceBuilderExtensions.WithHttpHealthCheck<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T>! builder, string? path = null, int? statusCode = null, string? endpointName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T>!
static Aspire.Hosting.ResourceBuilderExtensions.WithHttpsHealthCheck<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T>! builder, string? path = null, int? statusCode = null, string? endpointName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T>!
static Aspire.Hosting.ResourceBuilderExtensions.WithParentRelationship<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T>! builder, Aspire.Hosting.ApplicationModel.IResource! parent) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T>!
static Aspire.Hosting.ResourceBuilderExtensions.WithRelationship<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T>! builder, Aspire.Hosting.ApplicationModel.IResource! resource, string! type) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T>!
static Aspire.Hosting.Utils.VolumeNameGenerator.Generate<T>(Aspire.Hosting.ApplicationModel.IResourceBuilder<T>! builder, string! suffix) -> string!
static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Exited -> string!
Expand Down
30 changes: 30 additions & 0 deletions src/Aspire.Hosting/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1064,4 +1064,34 @@ public static IResourceBuilder<T> WithRelationship<T>(

return builder.WithAnnotation(new ResourceRelationshipAnnotation(resource, type));
}

/// <summary>
/// Adds a <see cref="ResourceRelationshipAnnotation"/> to the resource annotations to add a parent-child relationship.
/// </summary>
/// <typeparam name="T">The type of the resource.</typeparam>
/// <param name="builder">The resource builder.</param>
/// <param name="parent">The parent of <paramref name="builder"/>.</param>
/// <returns>A resource builder.</returns>
/// <remarks>
/// <para>
/// The <c>WithParentRelationship</c> method is used to add parent relationships to the resource. Relationships are used to link
/// resources together in UI.
/// </para>
/// </remarks>
/// <example>
/// This example shows adding a relationship between two resources.
/// <code lang="C#">
/// var builder = DistributedApplication.CreateBuilder(args);
/// var backend = builder.AddProject&lt;Projects.Backend&gt;("backend");
///
/// var frontend = builder.AddProject&lt;Projects.Manager&gt;("frontend")
/// .WithParentRelationship(backend.Resource);
/// </code>
/// </example>
public static IResourceBuilder<T> WithParentRelationship<T>(
this IResourceBuilder<T> builder,
IResource parent) where T : IResource
{
return builder.WithRelationship(parent, KnownRelationshipTypes.Parent);
}
}
33 changes: 33 additions & 0 deletions src/Shared/CustomResourceSnapshotExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,37 @@ internal static ImmutableArray<ResourcePropertySnapshot> SetResourceProperty(thi
// Add property.
return [.. properties, new ResourcePropertySnapshot(name, value)];
}

internal static ImmutableArray<ResourcePropertySnapshot> SetResourcePropertyRange(this ImmutableArray<ResourcePropertySnapshot> properties, IEnumerable<ResourcePropertySnapshot> newValues)
{
var existingProperties = new List<ResourcePropertySnapshot>(properties);
var propertiesToAdd = new List<ResourcePropertySnapshot>();

foreach (var newValue in newValues)
{
var found = false;
for (var i = 0; i < existingProperties.Count; i++)
{
var existingProperty = existingProperties[i];

if (string.Equals(existingProperty.Name, newValue.Name, StringComparisons.ResourcePropertyName))
{
if (existingProperty.Value != newValue.Value)
{
existingProperties[i] = existingProperty with { Value = newValue.Value };
}

found = true;
break;
}
}

if (!found)
{
propertiesToAdd.Add(newValue);
}
}

return [.. existingProperties, .. propertiesToAdd];
}
}

0 comments on commit c7745e6

Please sign in to comment.