Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/8.0] Add support for specifying custom container run arguments #3566

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// 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;

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Represents an additional argument to pass to the container run command.
/// </summary>
[DebuggerDisplay("Type = {GetType().Name,nq}")]
public sealed class ContainerRunArgsCallbackAnnotation : IResourceAnnotation
{
/// <summary>
/// Initializes a new instance of the <see cref="ContainerRunArgsCallbackAnnotation"/> class with the specified callback action.
/// </summary>
/// <param name="callback"></param>
public ContainerRunArgsCallbackAnnotation(Func<ContainerRunArgsCallbackContext, Task> callback)
{
ArgumentNullException.ThrowIfNull(callback);

Callback = callback;
}

/// <summary>
/// Initializes a new instance of the <see cref="ContainerRunArgsCallbackAnnotation"/> class with the specified callback action.
/// </summary>
/// <param name="callback">The callback action to be executed.</param>
public ContainerRunArgsCallbackAnnotation(Action<IList<object>> callback)
{
ArgumentNullException.ThrowIfNull(callback);

Callback = (c) =>
{
callback(c.Args);
return Task.CompletedTask;
};
}

/// <summary>
/// Gets the callback action to be executed when the executable arguments are parsed.
/// </summary>
public Func<ContainerRunArgsCallbackContext, Task> Callback { get; }
}

/// <summary>
/// Represents a callback context for the list of command-line arguments to be passed to the container run command.
/// </summary>
/// <param name="args">The list of command-line arguments.</param>
/// <param name="cancellationToken">The cancellation token associated with this execution.</param>
public sealed class ContainerRunArgsCallbackContext(IList<object> args, CancellationToken cancellationToken = default)
{
/// <summary>
/// Gets the list of command-line arguments.
/// </summary>
public IList<object> Args { get; } = args ?? throw new ArgumentNullException(nameof(args));

/// <summary>
/// Gets the cancellation token associated with the callback context.
/// </summary>
public CancellationToken CancellationToken { get; } = cancellationToken;
}
41 changes: 41 additions & 0 deletions src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,47 @@ public static IResourceBuilder<T> WithImageSHA256<T>(this IResourceBuilder<T> bu
return ThrowResourceIsNotContainer(builder);
}

/// <summary>
/// Adds a callback to be executed with a list of arguments to add to the container run command when a container resource is started.
/// </summary>
/// <typeparam name="T">The resource type.</typeparam>
/// <param name="builder">The resource builder.</param>
/// <param name="args">The arguments to be passed to the container run command when the container resource is started.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<T> WithContainerRunArgs<T>(this IResourceBuilder<T> builder, params string[] args) where T : ContainerResource
{
return builder.WithContainerRunArgs(context => context.Args.AddRange(args));
}

/// <summary>
/// Adds a callback to be executed with a list of arguments to add to the container run command when a container resource is started.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="builder">The resource builder.</param>
/// <param name="callback">A callback that allows for deferred execution for computing arguments. This runs after resources have been allocation by the orchestrator and allows access to other resources to resolve computed data, e.g. connection strings, ports.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<T> WithContainerRunArgs<T>(this IResourceBuilder<T> builder, Action<ContainerRunArgsCallbackContext> callback) where T : ContainerResource
{
return builder.WithContainerRunArgs(context =>
{
callback(context);
return Task.CompletedTask;
});
}

/// <summary>
/// Adds a callback to be executed with a list of arguments to add to the container run command when a container resource is started.
/// </summary>
/// <typeparam name="T">The resource type.</typeparam>
/// <param name="builder">The resource builder.</param>
/// <param name="callback">A callback that allows for deferred execution for computing arguments. This runs after resources have been allocation by the orchestrator and allows access to other resources to resolve computed data, e.g. connection strings, ports.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<T> WithContainerRunArgs<T>(this IResourceBuilder<T> builder, Func<ContainerRunArgsCallbackContext, Task> callback) where T : ContainerResource
{
var annotation = new ContainerRunArgsCallbackAnnotation(callback);
return builder.WithAnnotation(annotation);
}

private static IResourceBuilder<T> ThrowResourceIsNotContainer<T>(IResourceBuilder<T> builder) where T : ContainerResource
{
throw new InvalidOperationException($"The resource '{builder.Resource.Name}' does not have a container image specified. Use WithImage to specify the container image and tag.");
Expand Down
31 changes: 31 additions & 0 deletions src/Aspire.Hosting/Dcp/ApplicationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1566,6 +1566,37 @@ private async Task CreateContainerAsync(AppResource cr, ILogger resourceLogger,
}
}

// Apply optional extra arguments to the container run command.
if (modelContainerResource.TryGetAnnotationsOfType<ContainerRunArgsCallbackAnnotation>(out var runArgsCallback))
{
dcpContainerResource.Spec.RunArgs ??= [];

var args = new List<object>();

var containerRunArgsContext = new ContainerRunArgsCallbackContext(args, cancellationToken);

foreach (var callback in runArgsCallback)
{
await callback.Callback(containerRunArgsContext).ConfigureAwait(false);
}

foreach (var arg in args)
{
var value = arg switch
{
string s => s,
IValueProvider valueProvider => await GetValue(key: null, valueProvider, resourceLogger, isContainer: true, cancellationToken).ConfigureAwait(false),
null => null,
_ => throw new InvalidOperationException($"Unexpected value for {arg}")
};

if (value is not null)
{
dcpContainerResource.Spec.RunArgs.Add(value);
}
}
}

var failedToApplyArgs = false;
if (modelContainerResource.TryGetAnnotationsOfType<CommandLineArgsCallbackAnnotation>(out var argsCallback))
{
Expand Down
4 changes: 4 additions & 0 deletions src/Aspire.Hosting/Dcp/Model/Container.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ internal sealed class ContainerSpec
// Arguments to pass to the command that starts the container
[JsonPropertyName("args")]
public List<string>? Args { get; set; }

// Additional arguments to pass to the container run command
[JsonPropertyName("runArgs")]
public List<string>? RunArgs { get; set; }
}

internal static class VolumeMountType
Expand Down
4 changes: 3 additions & 1 deletion tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ public async Task VerifyDockerAppWorks()
testProgram.AppBuilder.Services.AddLogging(b => b.AddXunit(_testOutputHelper));

testProgram.AppBuilder.AddContainer("redis-cli", "redis")
.WithArgs("redis-cli", "-h", "host.docker.internal", "-p", "9999", "MONITOR");
.WithArgs("redis-cli", "-h", "host.docker.internal", "-p", "9999", "MONITOR")
.WithContainerRunArgs("--add-host", "testlocalhost:127.0.0.1");

await using var app = testProgram.Build();

Expand All @@ -236,6 +237,7 @@ public async Task VerifyDockerAppWorks()
{
Assert.Equal("redis:latest", item.Spec.Image);
Assert.Equal(["redis-cli", "-h", "host.docker.internal", "-p", "9999", "MONITOR"], item.Spec.Args);
Assert.Equal(["--add-host", "testlocalhost:127.0.0.1"], item.Spec.RunArgs);
});

await app.StopAsync();
Expand Down