Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/ContainerExecutableResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Executable resource that runs in a container.
/// </summary>
internal class ContainerExecutableResource(string name, ContainerResource containerResource, string command, string? workingDirectory)
: Resource(name), IResourceWithEnvironment, IResourceWithArgs, IResourceWithEndpoints, IResourceWithWaitSupport
Copy link
Member

Choose a reason for hiding this comment

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

@DeagleGross this merged in before I got a chance to fully review it - but one thing we should do here is ditch the TargetContainerResource property and make this resource implement the IResourceWithParent interface.

Copy link
Member

Choose a reason for hiding this comment

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

We should also get rid of the Args property and instead make it work similar to the ExecutableResource. Because you are implementing IResourceWithArgs it means that this resource should be able to work with the WithArgs method - and it should honor all of the semantics around deferred evaluation etc.

{
/// <summary>
/// Gets the command associated with this executable resource.
/// </summary>
public string Command { get; } = ThrowIfNullOrEmpty(command);

/// <summary>
/// Gets the working directory for the executable resource.
/// </summary>
public string? WorkingDirectory { get; } = workingDirectory;

/// <summary>
/// Args of the command to run in the container.
/// </summary>
public ICollection<string>? Args { get; init; }

/// <summary>
/// Target container resource that this executable runs in.
/// </summary>
public ContainerResource? TargetContainerResource { get; } = containerResource ?? throw new ArgumentNullException(nameof(containerResource));

private static string ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
{
ArgumentException.ThrowIfNullOrEmpty(argument, paramName);
return argument;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public static Task InitializeDcpAnnotations(BeforeStartEvent beforeStartEvent, C
nameGenerator.EnsureDcpInstancesPopulated(executable);
}

foreach (var containerExec in beforeStartEvent.Model.GetContainerExecutableResources())
{
nameGenerator.EnsureDcpInstancesPopulated(containerExec);
}

foreach (var project in beforeStartEvent.Model.GetProjectResources())
{
nameGenerator.EnsureDcpInstancesPopulated(project);
Expand Down
24 changes: 24 additions & 0 deletions src/Aspire.Hosting/ContainerExecutableResourceExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting;

/// <summary>
/// Provides extension methods for working with <see cref="ExecutableResource"/> objects.
/// </summary>
internal static class ContainerExecutableResourceExtensions
{
/// <summary>
/// Returns an enumerable collection of executable resources from the specified distributed application model.
/// </summary>
/// <param name="model">The distributed application model to retrieve executable resources from.</param>
/// <returns>An enumerable collection of executable resources.</returns>
public static IEnumerable<ContainerExecutableResource> GetContainerExecutableResources(this DistributedApplicationModel model)
{
ArgumentNullException.ThrowIfNull(model);

return model.Resources.OfType<ContainerExecutableResource>();
}
}
151 changes: 144 additions & 7 deletions src/Aspire.Hosting/Dcp/DcpExecutor.cs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Aspire.Hosting/Dcp/DcpNameGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public void EnsureDcpInstancesPopulated(IResource resource)
var (name, suffix) = GetContainerName(resource);
AddInstancesAnnotation(resource, [new DcpInstance(name, suffix, 0)]);
}
else if (resource is ExecutableResource)
else if (resource is ExecutableResource or ContainerExecutableResource)
{
var (name, suffix) = GetExecutableName(resource);
AddInstancesAnnotation(resource, [new DcpInstance(name, suffix, 0)]);
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting/Dcp/DcpResourceState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal sealed class DcpResourceState(Dictionary<string, IResource> application
{
public readonly ConcurrentDictionary<string, Container> ContainersMap = [];
public readonly ConcurrentDictionary<string, Executable> ExecutablesMap = [];
public readonly ConcurrentDictionary<string, ContainerExec> ContainerExecsMap = [];
public readonly ConcurrentDictionary<string, Service> ServicesMap = [];
public readonly ConcurrentDictionary<string, Endpoint> EndpointsMap = [];
public readonly ConcurrentDictionary<(string, string), List<string>> ResourceAssociatedServicesMap = [];
Expand Down
7 changes: 6 additions & 1 deletion src/Aspire.Hosting/Dcp/Model/ContainerExec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,18 +117,23 @@ public ContainerExec(ContainerExecSpec spec) : base(spec) { }
/// <param name="name">Resource name of the ContainerExec instance</param>
/// <param name="containerName">Resource name of the Container to run the command in</param>
/// <param name="command">The command name to run</param>
/// <param name="args">Arguments of the command to run</param>
/// <param name="workingDirectory">Container working directory to run the command in</param>
/// <returns>A new ContainerExec instance</returns>
public static ContainerExec Create(string name, string containerName, string command)
public static ContainerExec Create(string name, string containerName, string command, List<string>? args = null, string? workingDirectory = null)
{
var containerExec = new ContainerExec(new ContainerExecSpec
{
ContainerName = containerName,
Command = command,
Args = args,
WorkingDirectory = workingDirectory
})
{
Kind = Dcp.ContainerExecKind,
ApiVersion = Dcp.GroupVersion.ToString()
};

containerExec.Metadata.Name = name;
containerExec.Metadata.NamespaceProperty = string.Empty;

Expand Down
37 changes: 37 additions & 0 deletions src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,43 @@ ContainerLifetime GetContainerLifetime()
}
}

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 urls = GetUrls(executable, executable.Status?.State);
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);

return previous with
{
ResourceType = KnownResourceTypes.Executable,
State = state,
ExitCode = executable.Status?.ExitCode,
Properties = previous.Properties.SetResourcePropertyRange([
new(KnownProperties.Executable.WorkDir, executable.Spec.WorkingDirectory),
new(KnownProperties.Executable.Args, executable.Status?.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(),
Urls = urls,
Relationships = relationships
};
}

public CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceSnapshot previous)
{
string? projectPath = null;
Expand Down
34 changes: 32 additions & 2 deletions src/Aspire.Hosting/Exec/ExecResourceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,13 @@ IResource BuildResource(IResource targetExecResource)
{
return targetExecResource switch
{
ProjectResource prj => BuildAgainstProjectResource(prj),
ProjectResource prj => BuildAgainstResource(prj),
ContainerResource container => BuildAgainstResource(container),
_ => throw new InvalidOperationException($"Target resource {targetExecResource.Name} does not support exec mode.")
};
}

private IResource BuildAgainstProjectResource(ProjectResource project)
private IResource BuildAgainstResource(ProjectResource project)
{
var projectMetadata = project.GetProjectMetadata();
var projectDir = Path.GetDirectoryName(projectMetadata.ProjectPath) ?? throw new InvalidOperationException("Project path is invalid.");
Expand Down Expand Up @@ -238,4 +239,33 @@ annotation is EnvironmentAnnotation or EnvironmentCallbackAnnotation
return CommandLineArgsParser.ParseCommand(commandUnwrapped);
}
}

private IResource BuildAgainstResource(ContainerResource container)
{
var (exe, args) = ParseCommand();
string execResourceName = container.Name + "-exec";

// we cant resolve dcp name of container resource here - too early in the startup pipeline
// it will be resolved later in the Dcp layer
var containerExecutable = new ContainerExecutableResource(execResourceName, container, exe, workingDirectory: null)
{
Args = args
};

containerExecutable.Annotations.Add(new WaitAnnotation(container, waitType: WaitType.WaitUntilHealthy));

_logger.LogDebug("Exec container resource '{ResourceName}' will run command '{Command}' with {ArgsCount} args '{Args}'.", execResourceName, exe, args?.Length ?? 0, string.Join(' ', args ?? []));
return containerExecutable;

(string exe, string[] args) ParseCommand()
{
// cli wraps the command into the string with quotes
// to keep the command as a single argument
var command = _execOptions.Command;
var commandUnwrapped = command.AsSpan(1, command.Length - 2).ToString();
Debug.Assert(command[0] == '"' && command[^1] == '"');

return CommandLineArgsParser.ParseCommand(commandUnwrapped);
}
}
}
1 change: 1 addition & 0 deletions src/Shared/Model/KnownResourceTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Aspire.Dashboard.Model;
internal static class KnownResourceTypes
{
public const string Executable = "Executable";
public const string ContainerExec = "ContainerExec";
public const string Project = "Project";
public const string Container = "Container";
public const string Parameter = "Parameter";
Expand Down
6 changes: 0 additions & 6 deletions tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@
<ProjectReference Include="..\..\src\Aspire.Hosting\Aspire.Hosting.csproj" />
<ProjectReference Include="..\..\src\Aspire.Cli\Aspire.Cli.csproj" />
<ProjectReference Include="..\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj" />
<ProjectReference Include="..\Aspire.TestUtilities\Aspire.TestUtilities.csproj" IsAspireProjectResource="false" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\playground\DatabaseMigration\DatabaseMigration.ApiService\DatabaseMigration.ApiService.csproj" />
<ProjectReference Include="..\..\playground\DatabaseMigration\DatabaseMigration.AppHost\DatabaseMigration.AppHost.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading