Skip to content

Commit

Permalink
Add resource relationship to app model
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK committed Aug 15, 2024
1 parent 6173930 commit dff7497
Show file tree
Hide file tree
Showing 18 changed files with 151 additions and 12 deletions.
19 changes: 19 additions & 0 deletions src/Aspire.Dashboard/Model/ResourceViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public sealed class ResourceViewModel
public required DateTime? CreationTimeStamp { get; init; }
public required ImmutableArray<EnvironmentVariableViewModel> Environment { get; init; }
public required ImmutableArray<UrlViewModel> Urls { get; init; }
public required ImmutableArray<RelationshipViewModel> Relationships { get; init; }
public required FrozenDictionary<string, Value> Properties { get; init; }
public required ImmutableArray<CommandViewModel> Commands { get; init; }
public KnownResourceState? KnownState { get; init; }
Expand Down Expand Up @@ -113,3 +114,21 @@ public UrlViewModel(string name, Uri url, bool isInternal)
IsInternal = isInternal;
}
}

[DebuggerDisplay("ResourceName = {ResourceName}, Type = {Type}, Description = {Description}")]
public sealed class RelationshipViewModel
{
public string ResourceName { get; }
public string Type { get; }
public string? Description { get; }

public RelationshipViewModel(string resourceName, string type, string? description)
{
ArgumentException.ThrowIfNullOrWhiteSpace(resourceName);
ArgumentException.ThrowIfNullOrWhiteSpace(type);

ResourceName = resourceName;
Type = type;
Description = description;
}
}
8 changes: 8 additions & 0 deletions src/Aspire.Dashboard/ResourceService/Partials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public ResourceViewModel ToViewModel()
Properties = Properties.ToFrozenDictionary(property => ValidateNotNull(property.Name), property => ValidateNotNull(property.Value), StringComparers.ResourcePropertyName),
Environment = GetEnvironment(),
Urls = GetUrls(),
Relationships = GetRelationships(),
State = HasState ? State : null,
KnownState = HasState ? Enum.TryParse(State, out KnownResourceState knownState) ? knownState : null : null,
StateStyle = HasStateStyle ? StateStyle : null,
Expand All @@ -38,6 +39,13 @@ ImmutableArray<EnvironmentVariableViewModel> GetEnvironment()
.ToImmutableArray();
}

ImmutableArray<RelationshipViewModel> GetRelationships()
{
return Relationships
.Select(r => new RelationshipViewModel(r.ResourceName, r.Type, r.Description))
.ToImmutableArray();
}

ImmutableArray<UrlViewModel> GetUrls()
{
// Filter out bad urls
Expand Down
8 changes: 7 additions & 1 deletion src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ public static IResourceBuilder<PostgresDatabaseResource> AddDatabase(this IResou

builder.Resource.AddDatabase(name, databaseName);
var postgresDatabase = new PostgresDatabaseResource(name, databaseName, builder.Resource);
return builder.ApplicationBuilder.AddResource(postgresDatabase);
var databaseBuilder = builder.ApplicationBuilder.AddResource(postgresDatabase);

databaseBuilder.WithRelationship(builder.Resource, "Database", databaseName);

return databaseBuilder;
}

/// <summary>
Expand Down Expand Up @@ -150,6 +154,8 @@ public static IResourceBuilder<T> WithPgAdmin<T>(this IResourceBuilder<T> builde

configureContainer?.Invoke(pgAdminContainerBuilder);

pgAdminContainerBuilder.WithRelationship(builder.Resource, "Admin");

return builder;
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ public static IResourceBuilder<RedisResource> WithRedisCommander(this IResourceB

configureContainer?.Invoke(resourceBuilder);

resourceBuilder.WithRelationship(builder.Resource, "Manager");

return builder;
}
}
Expand Down
28 changes: 28 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public sealed record CustomResourceSnapshot
/// The URLs that should show up in the dashboard for this resource.
/// </summary>
public ImmutableArray<UrlSnapshot> Urls { get; init; } = [];

#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable RS0016 // Add public types and members to the declared API
public ImmutableArray<RelationshipSnapshot> Relationships { get; init; } = [];
#pragma warning restore RS0016 // Add public types and members to the declared API
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
}

/// <summary>
Expand Down Expand Up @@ -77,6 +83,12 @@ public sealed record EnvironmentVariableSnapshot(string Name, string? Value, boo
/// <param name="IsInternal">Determines if this url is internal.</param>
public sealed record UrlSnapshot(string Name, string Url, bool IsInternal);

#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable RS0016 // Add public types and members to the declared API
public sealed record RelationshipSnapshot(string ResourceName, string Type, string? Description);
#pragma warning restore RS0016 // Add public types and members to the declared API
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member

/// <summary>
/// A snapshot of the resource property.
/// </summary>
Expand Down Expand Up @@ -150,3 +162,19 @@ public static class KnownResourceStates
/// </summary>
public static readonly string Finished = nameof(Finished);
}

internal static class ResourceSnapshotBuilder
{
public static ImmutableArray<RelationshipSnapshot> BuildRelationships(IResource resource)
{
var relationships = ImmutableArray.CreateBuilder<RelationshipSnapshot>();

foreach (var annotation in resource.Annotations.OfType<ResourceRelationshipAnnotation>())
{
// TODO: Improve name
relationships.Add(new(annotation.Resource.Name, annotation.Type, annotation.Description));
}

return relationships.ToImmutable();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ private static CustomResourceSnapshot GetCurrentSnapshot(IResource resource, Res
previousState ??= new CustomResourceSnapshot()
{
ResourceType = resource.GetType().Name,
Relationships = ResourceSnapshotBuilder.BuildRelationships(resource),
Properties = []
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting.ApplicationModel;

#pragma warning disable RS0016 // Add public types and members to the declared API
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public class ResourceRelationshipAnnotation(IResource resource, string type, string? description) : IResourceAnnotation
{
public IResource Resource { get; } = resource;

public string Type { get; } = type;

public string? Description { get; } = description;
}
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
#pragma warning restore RS0016 // Add public types and members to the declared API
1 change: 1 addition & 0 deletions src/Aspire.Hosting/Dashboard/DashboardServiceData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ static GenericResourceSnapshot CreateResourceSnapshot(IResource resource, string
DisplayName = resource.Name,
Urls = snapshot.Urls,
Environment = snapshot.EnvironmentVariables,
Relationships = snapshot.Relationships,
ExitCode = snapshot.ExitCode,
State = snapshot.State?.Text,
StateStyle = snapshot.State?.Style
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/Dashboard/ResourceSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal abstract class ResourceSnapshot
public required int? ExitCode { get; init; }
public required DateTime? CreationTimeStamp { get; init; }
public required ImmutableArray<EnvironmentVariableSnapshot> Environment { get; init; }

public required ImmutableArray<RelationshipSnapshot> Relationships { get; init; }
public required ImmutableArray<UrlSnapshot> Urls { get; init; }

protected abstract IEnumerable<(string Key, Value Value)> GetProperties();
Expand Down
5 changes: 5 additions & 0 deletions src/Aspire.Hosting/Dashboard/proto/Partials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public static Resource FromSnapshot(ResourceSnapshot snapshot)
resource.Urls.Add(new Url { Name = url.Name, FullUrl = url.Url, IsInternal = url.IsInternal });
}

foreach (var relationships in snapshot.Relationships)
{
resource.Relationships.Add(new ResourceRelationship { ResourceName = relationships.ResourceName, Type = relationships.Type, Description = relationships.Description ?? string.Empty });
}

foreach (var property in snapshot.Properties)
{
resource.Properties.Add(new ResourceProperty { Name = property.Name, Value = property.Value });
Expand Down
14 changes: 13 additions & 1 deletion src/Aspire.Hosting/Dashboard/proto/resource_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ message Url {
bool is_internal = 3;
}

message ResourceRelationship {
// The name of the resource.
string resource_name = 1;
// The type of relationship.
string type = 2;
// Optional description.
optional string description = 3;
}

message ResourceProperty {
// Name of the data item, e.g. "container.id", "executable.pid", "project.path", ...
string name = 1;
Expand Down Expand Up @@ -140,13 +149,16 @@ message Resource {
// - Projects: process_id, project_path
repeated ResourceProperty properties = 12;

// The list of urls that this resource exposes
// The list of urls that this resource exposes.
repeated Url urls = 13;

// The style of the state. This is used to determine the state icon.
// Supported styles are "success", "info", "warning" and "error". Any other style
// will be treated as "unknown".
optional string state_style = 14;

// The list of relationships for this resource.
repeated ResourceRelationship relationships = 15;
}

////////////////////////////////////////////
Expand Down
25 changes: 21 additions & 4 deletions src/Aspire.Hosting/Dcp/ApplicationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,13 @@ private CustomResourceSnapshot ToSnapshot(Container container, CustomResourceSna
var environment = GetEnvironmentVariables(container.Status?.EffectiveEnv ?? container.Spec.Env, container.Spec.Env);
var state = container.AppModelInitialState == KnownResourceStates.Hidden ? KnownResourceStates.Hidden : container.Status?.State;

var relationships = ImmutableArray<RelationshipSnapshot>.Empty;
if (container.AppModelResourceName is not null &&
_applicationModel.TryGetValue(container.AppModelResourceName, out var appModelResource))
{
relationships = ResourceSnapshotBuilder.BuildRelationships(appModelResource);
}

return previous with
{
ResourceType = KnownResourceTypes.Container,
Expand All @@ -602,7 +609,8 @@ private CustomResourceSnapshot ToSnapshot(Container container, CustomResourceSna
],
EnvironmentVariables = environment,
CreationTimeStamp = container.Metadata.CreationTimestamp?.ToLocalTime(),
Urls = urls
Urls = urls,
Relationships = relationships
};

ImmutableArray<int> GetPorts()
Expand All @@ -627,9 +635,10 @@ ImmutableArray<int> GetPorts()
private CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceSnapshot previous)
{
string? projectPath = null;
IResource? appModelResource = null;

if (executable.AppModelResourceName is not null &&
_applicationModel.TryGetValue(executable.AppModelResourceName, out var appModelResource))
_applicationModel.TryGetValue(executable.AppModelResourceName, out appModelResource))
{
projectPath = appModelResource is ProjectResource p ? p.GetProjectMetadata().ProjectPath : null;
}
Expand All @@ -640,6 +649,12 @@ private CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceS

var environment = GetEnvironmentVariables(executable.Status?.EffectiveEnv, executable.Spec.Env);

var relationships = ImmutableArray<RelationshipSnapshot>.Empty;
if (appModelResource != null)
{
relationships = ResourceSnapshotBuilder.BuildRelationships(appModelResource);
}

if (projectPath is not null)
{
return previous with
Expand All @@ -656,7 +671,8 @@ private CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceS
],
EnvironmentVariables = environment,
CreationTimeStamp = executable.Metadata.CreationTimestamp?.ToLocalTime(),
Urls = urls
Urls = urls,
Relationships = relationships
};
}

Expand All @@ -673,7 +689,8 @@ private CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceS
],
EnvironmentVariables = environment,
CreationTimeStamp = executable.Metadata.CreationTimestamp?.ToLocalTime(),
Urls = urls
Urls = urls,
Relationships = relationships
};
}

Expand Down
19 changes: 19 additions & 0 deletions src/Aspire.Hosting/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ public static IResourceBuilder<T> WithEnvironment<T>(this IResourceBuilder<T> bu
/// <returns>A resource configured with the environment variable callback.</returns>
public static IResourceBuilder<T> WithEnvironment<T>(this IResourceBuilder<T> builder, string name, EndpointReference endpointReference) where T : IResourceWithEnvironment
{
builder.WithRelationship(builder.Resource, "Endpoint", endpointReference.EndpointName);

return builder.WithEnvironment(context =>
{
context.EnvironmentVariables[name] = endpointReference;
Expand Down Expand Up @@ -277,6 +279,8 @@ public static IResourceBuilder<TDestination> WithReference<TDestination>(this IR
var resource = source.Resource;
connectionName ??= resource.Name;

builder.WithRelationship(resource, "ConnectionString", connectionName);

return builder.WithEnvironment(context =>
{
var connectionStringName = resource.ConnectionStringEnvironmentVariable ?? $"{ConnectionStringEnvironmentName}{connectionName}";
Expand Down Expand Up @@ -343,6 +347,8 @@ public static IResourceBuilder<TDestination> WithReference<TDestination>(this IR
private static void ApplyEndpoints<T>(this IResourceBuilder<T> builder, IResourceWithEndpoints resourceWithEndpoints, string? endpointName = null)
where T : IResourceWithEnvironment
{
builder.WithRelationship(resourceWithEndpoints, "Endpoint", endpointName);

// When adding an endpoint we get to see whether there is an EndpointReferenceAnnotation
// on the resource, if there is then it means we have already been here before and we can just
// skip this and note the endpoint that we want to apply to the environment in the future
Expand Down Expand Up @@ -549,4 +555,17 @@ public static IResourceBuilder<T> ExcludeFromManifest<T>(this IResourceBuilder<T
{
return builder.WithAnnotation(ManifestPublishingCallbackAnnotation.Ignore);
}

#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable RS0016 // Add public types and members to the declared API
public static IResourceBuilder<T> WithRelationship<T>(
this IResourceBuilder<T> builder,
IResource resource,
string type,
string? description = null) where T : IResource
{
return builder.WithAnnotation(new ResourceRelationshipAnnotation(resource, "Endpoint", description));
}
#pragma warning restore RS0016 // Add public types and members to the declared API
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
Expand Down Expand Up @@ -114,6 +114,7 @@ private static ResourceViewModel CreateResourceViewModel(string appName, KnownRe
Environment = [],
Properties = FrozenDictionary<string, Value>.Empty,
Urls = [],
Relationships = [],
State = state?.ToString(),
KnownState = state,
StateStyle = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Frozen;
Expand Down Expand Up @@ -28,8 +28,8 @@ public sealed class MockDashboardClient : IDashboardClient
State = "Running",
Uid = Guid.NewGuid().ToString(),
StateStyle = null,
Urls = ImmutableArray<UrlViewModel>.Empty,

Urls = [],
Relationships = []
};

public bool IsEnabled => true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ private static ResourceViewModel CreateResource(ImmutableArray<UrlViewModel> url
CreationTimeStamp = DateTime.UtcNow,
Environment = [],
Urls = urls,
Relationships = [],
Properties = FrozenDictionary<string, Value>.Empty,
State = null,
KnownState = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ private static ResourceViewModel CreateResource(string name, string? serviceAddr
Environment = [],
Properties = FrozenDictionary<string, Value>.Empty,
Urls = servicePort is null || servicePort is null ? [] : [new UrlViewModel(name, new($"http://{serviceAddress}:{servicePort}"), isInternal: false)],
Relationships = [],
State = null,
KnownState = null,
StateStyle = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ private static GenericResourceSnapshot CreateResourceSnapshot(string name)
CreationTimeStamp = null,
DisplayName = "",
Urls = [],
Environment = []
Environment = [],
Relationships = []
};
}
}

0 comments on commit dff7497

Please sign in to comment.