// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#pragma warning disable ASPIREEXTENSION001 // Launch configuration metadata is experimental but needed for snapshot serialization.
#pragma warning disable ASPIREPROJECTS001 // Project launch defaults supply the configuration of coordinated process launches.
using System.Collections.Immutable;
using Aspire.Dashboard.Model;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dashboard;
using Aspire.Hosting.Dcp.Model;
namespace Aspire.Hosting.Dcp;
internal class ResourceSnapshotBuilder
{
private readonly DcpResourceState _resourceState;
public ResourceSnapshotBuilder(DcpResourceState resourceState)
{
_resourceState = resourceState;
}
public CustomResourceSnapshot ToSnapshot(Container container, CustomResourceSnapshot previous)
{
var containerId = container.Status?.ContainerId;
var urls = GetUrls(container, container.Status?.State);
var volumes = GetVolumes(container);
var effectiveArgs = container.Status?.EffectiveArgs;
var environment = GetEnvironmentVariables(container.Status?.EffectiveEnv ?? container.Spec.Env, container.Spec.Env);
var state = container.Status?.State;
if (container.Spec.Start is false && (state == null || state == ContainerState.Pending))
{
state = KnownResourceStates.NotStarted;
}
var relationships = ImmutableArray<RelationshipSnapshot>.Empty;
(ImmutableArray<string> Args, ImmutableArray<int>? ArgsAreSensitive, bool IsSensitive)? launchArguments = null;
if (container.AppModelResourceName is not null &&
_resourceState.ApplicationModel.TryGetValue(container.AppModelResourceName, out var appModelResource))
{
relationships = ApplicationModel.ResourceSnapshotBuilder.BuildRelationships(appModelResource);
launchArguments = GetLaunchArgs(container, effectiveArgs);
}
return previous with
{
ResourceType = previous.ResourceType ?? KnownResourceTypes.Container,
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 = previous.Properties.SetResourcePropertyRange([
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Container, KnownProperties.Container.Image, container.Spec.Image),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Container, KnownProperties.Container.Id, containerId),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Container, KnownProperties.Container.Command, container.Spec.Command),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Container, KnownProperties.Container.Args, effectiveArgs ?? [], isSensitive: true),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Container, KnownProperties.Container.Ports, GetPorts()),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Container, KnownProperties.Container.Lifetime, GetContainerLifetime()),
new(KnownProperties.Resource.AppArgs, launchArguments?.Args) { IsSensitive = launchArguments?.IsSensitive ?? false },
new(KnownProperties.Resource.AppArgsSensitivity, launchArguments?.ArgsAreSensitive) { IsSensitive = launchArguments?.IsSensitive ?? false },
]),
EnvironmentVariables = environment,
CreationTimeStamp = container.Metadata.CreationTimestamp?.ToUniversalTime(),
StartTimeStamp = container.Status?.StartupTimestamp?.ToUniversalTime(),
StopTimeStamp = container.Status?.FinishTimestamp?.ToUniversalTime(),
Urls = urls,
Volumes = volumes,
Relationships = relationships
};
ImmutableArray<int> GetPorts()
{
if (container.Spec.Ports is null)
{
return [];
}
var ports = ImmutableArray.CreateBuilder<int>();
foreach (var port in container.Spec.Ports)
{
if (port.ContainerPort != null)
{
ports.Add(port.ContainerPort.Value);
}
}
return ports.ToImmutable();
}
ContainerLifetime GetContainerLifetime()
{
return (container.Spec.Persistent ?? false) ? ContainerLifetime.Persistent : ContainerLifetime.Session;
}
}
public CustomResourceSnapshot ToSnapshot(ContainerExec executable, CustomResourceSnapshot previous)
{
IResource? appModelResource = null;
_ = executable.AppModelResourceName is not null && _resourceState.ApplicationModel.TryGetValue(executable.AppModelResourceName, out appModelResource);
var state = executable.AppModelInitialState is "Hidden" ? "Hidden" : executable.Status?.State;
var environment = GetEnvironmentVariables(executable.Status?.EffectiveEnv, executable.Spec.Env);
var effectiveArgs = executable.Status?.EffectiveArgs;
var launchArguments = GetLaunchArgs(executable, effectiveArgs);
var relationships = ImmutableArray<RelationshipSnapshot>.Empty;
if (appModelResource != null)
{
relationships = ApplicationModel.ResourceSnapshotBuilder.BuildRelationships(appModelResource);
}
return previous with
{
ResourceType = previous.ResourceType ?? KnownResourceTypes.Executable,
State = state,
ExitCode = executable.Status?.ExitCode,
Properties = previous.Properties.SetResourcePropertyRange([
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Executable, KnownProperties.Executable.WorkDir, executable.Spec.WorkingDirectory),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Executable, KnownProperties.Executable.Args, effectiveArgs ?? [], isSensitive: true),
new(KnownProperties.Resource.AppArgs, launchArguments?.Args) { IsSensitive = launchArguments?.IsSensitive ?? false },
new(KnownProperties.Resource.AppArgsSensitivity, launchArguments?.ArgsAreSensitive) { IsSensitive = launchArguments?.IsSensitive ?? false },
]),
EnvironmentVariables = environment,
CreationTimeStamp = executable.Metadata.CreationTimestamp?.ToUniversalTime(),
StartTimeStamp = executable.Status?.StartupTimestamp?.ToUniversalTime(),
StopTimeStamp = executable.Status?.FinishTimestamp?.ToUniversalTime(),
Relationships = relationships
};
}
public CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceSnapshot previous)
{
string? projectPath = null;
string? launchProfileName = null;
IResource? appModelResource = null;
if (executable.AppModelResourceName is not null &&
_resourceState.ApplicationModel.TryGetValue(executable.AppModelResourceName, out appModelResource))
{
if (appModelResource is ProjectResource projectResource)
{
projectPath = projectResource.GetProjectMetadata().ProjectPath;
launchProfileName = projectResource.GetEffectiveLaunchProfile()?.Name;
}
else if (appModelResource.TryGetProjectMetadata(out var projectMetadata))
{
// New-style, annotation-based C# service (DotnetProjectResource)
projectPath = projectMetadata.ProjectPath;
launchProfileName = appModelResource.GetEffectiveLaunchProfile()?.Name;
}
}
var state = executable.AppModelInitialState is "Hidden" ? "Hidden" : executable.Status?.State;
if (executable.Spec.Start is false && IsNotStartedExecutableState(state))
{
state = KnownResourceStates.NotStarted;
}
var urls = GetUrls(executable, executable.Status?.State);
var effectiveArgs = executable.Status?.EffectiveArgs;
var environment = GetEnvironmentVariables(executable.Status?.EffectiveEnv, executable.Spec.Env);
var relationships = ImmutableArray<RelationshipSnapshot>.Empty;
if (appModelResource != null)
{
relationships = ApplicationModel.ResourceSnapshotBuilder.BuildRelationships(appModelResource);
}
var launchArguments = GetLaunchArgs(executable, effectiveArgs);
var properties = previous.Properties;
// A language application's source does not change when terminal or persistent execution disables debugging.
if (appModelResource is ExecutableResource sourceResource
&& sourceResource.HasAnnotationOfType<SupportsDebuggingAnnotation>())
{
properties = properties.SetResourceProperty(KnownProperties.Resource.SourcePath, sourceResource.WorkingDirectory);
}
var launchConfigurationType = GetLaunchConfigurationType(appModelResource);
properties = launchConfigurationType is not null
? properties.SetResourceProperty(KnownProperties.Resource.LaunchConfigurationType, launchConfigurationType)
: properties.RemoveResourceProperty(KnownProperties.Resource.LaunchConfigurationType);
properties = properties
.RemoveResourceProperty(KnownProperties.Project.LaunchCommand)
.RemoveResourceProperty(KnownProperties.Project.Configuration)
.RemoveResourceProperty(KnownProperties.Project.TargetFramework)
.RemoveResourceProperty(KnownProperties.Project.TargetPath)
.RemoveResourceProperty(KnownProperties.Project.UseAppHost);
var dotNetLaunchProperties = GetDotNetLaunchProperties(executable, appModelResource, launchConfigurationType, effectiveArgs);
if (projectPath is not null)
{
return previous with
{
ResourceType = previous.ResourceType ?? KnownResourceTypes.Project,
State = state,
ExitCode = executable.Status?.ExitCode,
Properties = properties.SetResourcePropertyRange([
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Project, KnownProperties.Executable.Path, executable.Spec.ExecutablePath),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Project, KnownProperties.Executable.WorkDir, executable.Spec.WorkingDirectory),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Project, KnownProperties.Executable.Args, effectiveArgs ?? [], isSensitive: true),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Project, KnownProperties.Executable.Pid, executable.Status?.ProcessId),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Project, KnownProperties.Project.Path, projectPath),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Project, KnownProperties.Project.LaunchProfile, launchProfileName),
new(KnownProperties.Resource.AppArgs, launchArguments?.Args) { IsSensitive = launchArguments?.IsSensitive ?? false },
new(KnownProperties.Resource.AppArgsSensitivity, launchArguments?.ArgsAreSensitive) { IsSensitive = launchArguments?.IsSensitive ?? false },
.. dotNetLaunchProperties,
]),
EnvironmentVariables = environment,
CreationTimeStamp = executable.Metadata.CreationTimestamp?.ToUniversalTime(),
StartTimeStamp = executable.Status?.StartupTimestamp?.ToUniversalTime(),
StopTimeStamp = executable.Status?.FinishTimestamp?.ToUniversalTime(),
Urls = urls,
Relationships = relationships
};
}
return previous with
{
ResourceType = previous.ResourceType ?? KnownResourceTypes.Executable,
State = state,
ExitCode = executable.Status?.ExitCode,
Properties = properties.SetResourcePropertyRange([
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Executable, KnownProperties.Executable.Path, executable.Spec.ExecutablePath),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Executable, KnownProperties.Executable.WorkDir, executable.Spec.WorkingDirectory),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Executable, KnownProperties.Executable.Args, effectiveArgs ?? [], isSensitive: true),
ResourcePropertySnapshotMetadata.Create(KnownResourceTypes.Executable, KnownProperties.Executable.Pid, executable.Status?.ProcessId),
new(KnownProperties.Resource.AppArgs, launchArguments?.Args) { IsSensitive = launchArguments?.IsSensitive ?? false },
new(KnownProperties.Resource.AppArgsSensitivity, launchArguments?.ArgsAreSensitive) { IsSensitive = launchArguments?.IsSensitive ?? false },
]),
EnvironmentVariables = environment,
CreationTimeStamp = executable.Metadata.CreationTimestamp?.ToUniversalTime(),
StartTimeStamp = executable.Status?.StartupTimestamp?.ToUniversalTime(),
StopTimeStamp = executable.Status?.FinishTimestamp?.ToUniversalTime(),
Urls = urls,
Relationships = relationships
};
}
private static string? GetLaunchConfigurationType(IResource? resource)
{
// The annotation identifies the debugger even when the current IDE lacks it. Structural exclusions
// cannot be fixed by installing an extension, so do not advertise those resources as debuggable.
if (resource is not null
&& resource.TryGetLastAnnotation<SupportsDebuggingAnnotation>(out var annotation)
&& !resource.HasAnnotationOfType<ForceProcessExecutionAnnotation>()
&& !resource.HasPersistentLifetime())
{
return annotation.LaunchConfigurationType;
}
return null;
}
private static bool IsNotStartedExecutableState(string? state)
{
return string.IsNullOrEmpty(state) || state == ExecutableState.Unknown;
}
private static ImmutableArray<ResourcePropertySnapshot> GetDotNetLaunchProperties(
Executable resource,
IResource? appModelResource,
string? launchConfigurationType,
IReadOnlyList<string>? effectiveArgs)
{
if (resource.Spec.ExecutionType == ExecutionType.Process &&
launchConfigurationType == KnownLaunchConfigurationTypes.ProjectWithExternalBuild &&
appModelResource is not null &&
appModelResource.TryGetProjectMetadata(out var projectMetadata) &&
projectMetadata.SuppressBuild &&
!projectMetadata.IsFileBasedApp &&
projectMetadata.BuildWorkingDirectory is not null)
{
// Coordinated projects execute the SDK's RunCommand directly, which may be an apphost
// executable or "dotnet exec app.dll". The reported PID is the application, not a
// run/watch launcher, and its arguments must not be parsed as SDK build options.
appModelResource.TryGetLastAnnotation<ProjectLaunchDefaultsAnnotation>(out var launchDefaults);
var directProperties = ImmutableArray.CreateBuilder<ResourcePropertySnapshot>();
directProperties.Add(new(KnownProperties.Project.LaunchCommand, "direct"));
if (launchDefaults?.BuildConfiguration is { } directBuildConfiguration)
{
directProperties.Add(new(KnownProperties.Project.Configuration, directBuildConfiguration));
}
if (projectMetadata.BuildTarget is { } buildTarget)
{
directProperties.Add(new(KnownProperties.Project.TargetPath, buildTarget.TargetPath));
directProperties.Add(new(KnownProperties.Project.UseAppHost, buildTarget.UseAppHost));
}
return directProperties.ToImmutable();
}
var executableName = Path.GetFileName(resource.Spec.ExecutablePath);
if (!string.Equals(executableName, "dotnet", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(executableName, "dotnet.exe", StringComparison.OrdinalIgnoreCase))
{
return [];
}
// IDE launches report the application PID rather than a dotnet run/watch launcher.
// Their effective arguments belong to the application, so use structured project metadata.
if (resource.Spec.ExecutionType == ExecutionType.IDE &&
KnownLaunchConfigurationTypes.IsProject(launchConfigurationType) &&
resource.TryGetProjectLaunchConfiguration(out var launchConfiguration) &&
KnownLaunchConfigurationTypes.IsProject(launchConfiguration.Type))
{
return launchConfiguration.BuildConfiguration is { } buildConfiguration
? [new(KnownProperties.Project.LaunchCommand, "ide"), new(KnownProperties.Project.Configuration, buildConfiguration)]
: [new(KnownProperties.Project.LaunchCommand, "ide")];
}
if (effectiveArgs is null ||
FindDotNetProjectCommand(effectiveArgs) is not { } commandInfo)
{
return [new(KnownProperties.Project.LaunchCommand, null)];
}
var (command, commandIndex) = commandInfo;
var sensitiveArgumentIndexes = GetSensitiveEffectiveArgumentIndexes(resource);
if (sensitiveArgumentIndexes.Contains(commandIndex))
{
return [new(KnownProperties.Project.LaunchCommand, null)];
}
string? configuration = null;
string? targetFramework = null;
// DCP reports dotnet launch arguments as:
// ["watch", "--project", "/repo/api.csproj", "--configuration", "Release", "--framework=net10.0", "--", ...appArgs]
// ["run", "--configuration:Release", "-f:net10.0", "--", ...appArgs]
// ["[env:NAME=value]", "--diagnostics", "run", "--project", "/repo/api.csproj"]
// ["-d", "watch", "--project", "/repo/api.csproj"]
// Application arguments can contain unrelated values and remain sensitive in executable.args.
// Watch's forwarding separator is checked separately before treating "--" as the application boundary.
// See https://learn.microsoft.com/dotnet/core/tools/dotnet and
// https://github.com/dotnet/command-line-api/blob/main/src/System.CommandLine/EnvironmentVariablesDirective.cs.
for (var index = commandIndex + 1; index < effectiveArgs.Count; index++)
{
var argument = effectiveArgs[index];
if (argument == "--")
{
// Example: watch --no-hot-reload run -- --configuration Release.
// Older watch implementations strip the first "--"; newer ones retain the application boundary.
// Without the selected SDK, even preceding target options cannot disambiguate a nonempty suffix.
// See https://github.com/dotnet/sdk/blob/v8.0.426/src/BuiltInTools/dotnet-watch/CommandLineOptions.cs#L248-L314
// and https://github.com/dotnet/sdk/blob/v10.0.100/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs#L262-L307.
if (command == "watch" && index + 1 < effectiveArgs.Count)
{
return [new(KnownProperties.Project.LaunchCommand, null)];
}
break;
}
// Runtime, artifact layout, and MSBuild property overrides can change the output path or assembly identity.
// A hidden launcher input cannot be replaced by the evaluator's default either.
if (sensitiveArgumentIndexes.Contains(index) || ChangesUnreportedTargetInputs(argument))
{
return [new(KnownProperties.Project.LaunchCommand, null)];
}
if (TryReadOptionValue(argument, "--configuration", "-c", out var inlineConfiguration))
{
configuration = inlineConfiguration;
continue;
}
if (TryReadOptionValue(argument, "--framework", "-f", out var inlineTargetFramework))
{
targetFramework = inlineTargetFramework;
continue;
}
if (argument is "--configuration" or "-c")
{
configuration = ReadNextValue(effectiveArgs, sensitiveArgumentIndexes, ref index);
if (configuration is null)
{
return [new(KnownProperties.Project.LaunchCommand, null)];
}
continue;
}
if (argument is "--framework" or "-f")
{
targetFramework = ReadNextValue(effectiveArgs, sensitiveArgumentIndexes, ref index);
if (targetFramework is null)
{
return [new(KnownProperties.Project.LaunchCommand, null)];
}
}
}
var launchCommand = command.ToLowerInvariant();
var properties = ImmutableArray.CreateBuilder<ResourcePropertySnapshot>(3);
properties.Add(new(KnownProperties.Project.LaunchCommand, launchCommand));
if (configuration is not null)
{
properties.Add(new(KnownProperties.Project.Configuration, configuration));
}
if (targetFramework is not null)
{
properties.Add(new(KnownProperties.Project.TargetFramework, targetFramework));
}
return properties.ToImmutable();
static bool ChangesUnreportedTargetInputs(string argument)
{
// Examples: --runtime win-x64, --artifacts-path=custom, -p:OutputPath=custom, /property:AssemblyName=Custom.
// Both colon and equals spellings are accepted by the dotnet/MSBuild launchers.
var option = argument.Split(['=', ':'], 2)[0].ToLowerInvariant();
return option is "--runtime" or "-r" or "--arch" or "-a" or "--os" or "--artifacts-path"
or "--property" or "--properties" or "-p" or "/p" or "-property" or "/property";
}
static (string Command, int Index)? FindDotNetProjectCommand(IReadOnlyList<string> arguments)
{
var index = 0;
var hasEnvironmentVariableDirective = false;
while (index < arguments.Count &&
(string.Equals(arguments[index], "[env]", StringComparison.OrdinalIgnoreCase) ||
arguments[index].StartsWith("[env:", StringComparison.OrdinalIgnoreCase) && arguments[index].EndsWith(']')))
{
hasEnvironmentVariableDirective = true;
index++;
}
while (index < arguments.Count && arguments[index] is "-d" or "--diagnostics")
{
index++;
}
if (index >= arguments.Count)
{
return null;
}
return arguments[index].ToLowerInvariant() switch
{
"run" => ("run", index),
// .NET 10 cannot resolve the external watch command through an environment directive.
"watch" when !hasEnvironmentVariableDirective => ("watch", index),
_ => null,
};
}
static bool TryReadOptionValue(string argument, string longOption, string shortOption, out string? value)
{
foreach (var option in new[] { longOption, shortOption })
{
if (argument.Length > option.Length &&
argument.StartsWith(option, StringComparison.Ordinal) &&
argument[option.Length] is '=' or ':')
{
value = NormalizeValue(argument[(option.Length + 1)..]);
return true;
}
}
value = null;
return false;
}
static string? ReadNextValue(IReadOnlyList<string> arguments, HashSet<int> sensitiveArgumentIndexes, ref int index)
{
var optionIsSensitive = sensitiveArgumentIndexes.Contains(index);
if (index + 1 >= arguments.Count || arguments[index + 1] == "--")
{
return null;
}
index++;
return optionIsSensitive || sensitiveArgumentIndexes.Contains(index)
? null
: NormalizeValue(arguments[index]);
}
static string? NormalizeValue(string value)
{
var normalized = value.Trim();
return normalized.Length > 0 ? normalized : null;
}
}
private static HashSet<int> GetSensitiveEffectiveArgumentIndexes(CustomResource resource)
{
if (resource.TryGetAnnotationAsObjectList(
Executable.SensitiveEffectiveArgumentIndexesAnnotation,
out List<int>? sensitiveEffectiveArgumentIndexes))
{
return sensitiveEffectiveArgumentIndexes.ToHashSet();
}
if (!resource.TryGetAnnotationAsObjectList(
CustomResource.ResourceAppArgsAnnotation,
out List<AppLaunchArgumentAnnotation>? launchArgumentAnnotations))
{
return [];
}
return launchArgumentAnnotations
.Where(static annotation => annotation.IsSensitive && annotation.EffectiveArgumentIndex is not null)
.Select(static annotation => annotation.EffectiveArgumentIndex!.Value)
.ToHashSet();
}
private static (ImmutableArray<string> Args, ImmutableArray<int>? ArgsAreSensitive, bool IsSensitive)? GetLaunchArgs(CustomResource resource, IReadOnlyList<string>? effectiveArgs)
{
if (!resource.TryGetAnnotationAsObjectList(CustomResource.ResourceAppArgsAnnotation, out List<AppLaunchArgumentAnnotation>? launchArgumentAnnotations))
{
return null;
}
var launchArgsBuilder = ImmutableArray.CreateBuilder<string>();
var argsAreSensitiveBuilder = ImmutableArray.CreateBuilder<int>();
var anySensitive = false;
foreach (var annotation in launchArgumentAnnotations)
{
if (annotation.IsSensitive)
{
anySensitive = true;
}
launchArgsBuilder.Add(GetArgumentValue(annotation, effectiveArgs));
argsAreSensitiveBuilder.Add(Convert.ToInt32(annotation.IsSensitive));
}
return (launchArgsBuilder.ToImmutable(), argsAreSensitiveBuilder.ToImmutable(), anySensitive);
static string GetArgumentValue(AppLaunchArgumentAnnotation annotation, IReadOnlyList<string>? effectiveArgs)
{
if (annotation.EffectiveArgumentIndex is int index &&
effectiveArgs is not null &&
(uint)index < (uint)effectiveArgs.Count)
{
return effectiveArgs[index];
}
return annotation.Argument;
}
}
private ImmutableArray<UrlSnapshot> GetUrls(CustomResource resource, string? resourceState)
{
var urls = ImmutableArray.CreateBuilder<UrlSnapshot>();
var appModelResourceName = resource.AppModelResourceName;
if (appModelResourceName is string resourceName &&
_resourceState.ApplicationModel.TryGetValue(resourceName, out var appModelResource) &&
appModelResource.TryGetUrls(out var resourceUrls))
{
var endpointUrls = resourceUrls.Where(u => u.Endpoint is not null).ToList();
var nonEndpointUrls = resourceUrls.Where(u => u.Endpoint is null).ToList();
var resourceServices = _resourceState.AppResources.OfType<ServiceWithModelResource>()
.Where(r => r.Service.AppModelResourceName == resource.AppModelResourceName)
.Select(s => s.Service)
.ToList();
var name = resource.Metadata.Name;
// Add the endpoint URLs for endpoints belonging to the current resource
var serviceEndpoints = new HashSet<(string EndpointName, string ServiceMetadataName)>(
resourceServices
.Where(s => !string.IsNullOrEmpty(s.EndpointName))
.Select(s => (s.EndpointName!, s.Metadata.Name)));
var processedEndpointUrls = new HashSet<ResourceUrlAnnotation>();
foreach (var endpoint in serviceEndpoints)
{
var (endpointName, serviceName) = endpoint;
var urlsForEndpoint = endpointUrls.Where(u =>
string.Equals(endpointName, u.Endpoint?.EndpointName, StringComparisons.EndpointAnnotationName)
&& u.Endpoint?.Resource.Name == appModelResource.Name)
.ToList();
foreach (var endpointUrl in urlsForEndpoint)
{
var activeEndpoint = _resourceState.EndpointsMap.SingleOrDefault(e =>
e.Value.Spec.ServiceName == serviceName
&& e.Value.Metadata.OwnerReferences?.Any(or => or.Kind == resource.Kind && or.Name == name) == true)
.Value;
var isInactive = activeEndpoint is null;
urls.Add(
new(Name: endpointUrl.Endpoint!.EndpointName,
Url: endpointUrl.Url,
IsInternal:
endpointUrl.IsInternal)
{
IsInactive = isInactive,
DisplayProperties = new(endpointUrl.DisplayText ?? "", endpointUrl.DisplayOrder ?? 0)
});
processedEndpointUrls.Add(endpointUrl);
}
}
// Add endpoint URLs that reference endpoints from other resources
var crossResourceEndpointUrls = endpointUrls.Where(u => !processedEndpointUrls.Contains(u)).ToList();
var resourceRunning = string.Equals(resourceState, KnownResourceStates.Running, StringComparisons.ResourceState);
foreach (var endpointUrl in crossResourceEndpointUrls)
{
var endpointOwnerResourceName = endpointUrl.Endpoint!.Resource.Name;
var endpointName = endpointUrl.Endpoint.EndpointName;
// Find the DCP service representing the appmodel endpoint in the owning resource
var endpointOwnerEndpoint = _resourceState.AppResources.OfType<ServiceWithModelResource>()
.Where(r => r.Service.AppModelResourceName == endpointOwnerResourceName)
.Select(s => s.Service)
.FirstOrDefault(s => string.Equals(endpointName, s.EndpointName, StringComparisons.EndpointAnnotationName));
// The endpoint is active if there is an active DCP endpoint for the owning resource's service endpoint
var isActive = _resourceState.EndpointsMap.Any(e => e.Value.Spec.ServiceName == endpointOwnerEndpoint?.Metadata.Name);
urls.Add(
new(Name: endpointName, Url: endpointUrl.Url, IsInternal: endpointUrl.IsInternal)
{
IsInactive = !isActive,
DisplayProperties = new(endpointUrl.DisplayText ?? "", endpointUrl.DisplayOrder ?? 0)
});
}
// Add the non-endpoint URLs
foreach (var url in nonEndpointUrls)
{
urls.Add(
new(Name: null, Url: url.Url, IsInternal: url.IsInternal)
{
IsInactive = !resourceRunning,
DisplayProperties = new(url.DisplayText ?? "", url.DisplayOrder ?? 0)
});
}
}
return urls.ToImmutable();
}
private static ImmutableArray<VolumeSnapshot> GetVolumes(CustomResource resource)
{
if (resource is Container container)
{
return container.Spec.VolumeMounts?.Select(v => new VolumeSnapshot(v.Source, v.Target ?? "", v.Type, v.IsReadOnly)).ToImmutableArray() ?? [];
}
return [];
}
private static ImmutableArray<EnvironmentVariableSnapshot> GetEnvironmentVariables(List<EnvVar>? effectiveSource, List<EnvVar>? specSource)
{
if (effectiveSource is null or { Count: 0 })
{
return [];
}
var environment = ImmutableArray.CreateBuilder<EnvironmentVariableSnapshot>(effectiveSource.Count);
foreach (var env in effectiveSource)
{
if (env.Name is not null)
{
var isFromSpec = specSource?.Any(e => string.Equals(e.Name, env.Name, StringComparison.Ordinal)) is true or null;
environment.Add(new(env.Name, env.Value ?? "", isFromSpec));
}
}
environment.Sort((v1, v2) => string.Compare(v1.Name, v2.Name, StringComparison.Ordinal));
return environment.ToImmutable();
}
}