diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/GoModInstallerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Golang/GoModInstallerResource.cs new file mode 100644 index 00000000..64741561 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/GoModInstallerResource.cs @@ -0,0 +1,9 @@ +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a Go module installer that runs go mod tidy or go mod download. +/// +/// The name of the resource. +/// The working directory to use for the command. +public class GoModInstallerResource(string name, string workingDirectory) + : ExecutableResource(name, "go", workingDirectory); diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs index 964dc135..62d62dfe 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs @@ -234,4 +234,68 @@ private static string GetDefaultGoBaseImage(string workingDirectory, IServicePro logger.LogDebug("No Go version detected, will use default version"); return null; } + + /// + /// Ensures Go module dependencies are tidied before the application starts using go mod tidy. + /// + /// The Golang app resource builder. + /// Optional action to configure the installer resource. + /// A reference to the . + public static IResourceBuilder WithGoModTidy( + this IResourceBuilder builder, + Action>? configureInstaller = null) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + + // Only install packages during development, not in publish mode + if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) + { + var installerName = $"{builder.Resource.Name}-go-mod-tidy"; + var installer = new GoModInstallerResource(installerName, builder.Resource.WorkingDirectory); + + var installerBuilder = builder.ApplicationBuilder.AddResource(installer) + .WithArgs("mod", "tidy") + .WithParentRelationship(builder.Resource) + .ExcludeFromManifest(); + + // Make the parent resource wait for the installer to complete + builder.WaitForCompletion(installerBuilder); + + configureInstaller?.Invoke(installerBuilder); + } + + return builder; + } + + /// + /// Ensures Go module dependencies are downloaded before the application starts using go mod download. + /// + /// The Golang app resource builder. + /// Optional action to configure the installer resource. + /// A reference to the . + public static IResourceBuilder WithGoModDownload( + this IResourceBuilder builder, + Action>? configureInstaller = null) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + + // Only install packages during development, not in publish mode + if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode) + { + var installerName = $"{builder.Resource.Name}-go-mod-download"; + var installer = new GoModInstallerResource(installerName, builder.Resource.WorkingDirectory); + + var installerBuilder = builder.ApplicationBuilder.AddResource(installer) + .WithArgs("mod", "download") + .WithParentRelationship(builder.Resource) + .ExcludeFromManifest(); + + // Make the parent resource wait for the installer to complete + builder.WaitForCompletion(installerBuilder); + + configureInstaller?.Invoke(installerBuilder); + } + + return builder; + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md index e7de82f2..43416009 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md @@ -29,6 +29,45 @@ To have the Golang application listen on the correct port, you can use the follo r.Run(":"+os.Getenv("PORT")) ``` +## Dependency Management + +The integration provides support for Go module dependency management using `go mod tidy` or `go mod download`. + +### Using `go mod tidy` + +To run `go mod tidy` before your application starts (to clean up and verify dependencies): + +```csharp +var golang = builder.AddGolangApp("golang", "../gin-api") + .WithGoModTidy() + .WithHttpEndpoint(env: "PORT"); +``` + +### Using `go mod download` + +To run `go mod download` before your application starts (to download dependencies without verification): + +```csharp +var golang = builder.AddGolangApp("golang", "../gin-api") + .WithGoModDownload() + .WithHttpEndpoint(env: "PORT"); +``` + +Both methods create an installer resource that runs before your application starts, ensuring dependencies are available. The installer resource appears as a child resource in the Aspire dashboard. + +You can also customize the installer resource using the optional `configureInstaller` parameter: + +```csharp +var golang = builder.AddGolangApp("golang", "../gin-api") + .WithGoModTidy(configureInstaller: installer => + { + installer.WithEnvironment("GOPROXY", "https://proxy.golang.org,direct"); + }) + .WithHttpEndpoint(env: "PORT"); +``` + +> **Note:** The `WithGoModTidy` and `WithGoModDownload` methods only run during development. When publishing, the generated Dockerfile handles dependency management automatically. + ## Publishing When publishing your Aspire application, the Golang resource automatically generates a multi-stage Dockerfile for containerization. This means you don't need to manually create a Dockerfile for your Golang application. diff --git a/tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/ResourceCreationTests.cs index c0ed2d21..48aa310d 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/ResourceCreationTests.cs @@ -88,4 +88,98 @@ public async Task GolangAppWithExecutableAsync() } ); } + + [Fact] + public void GolangAppWithGoModTidyCreatesInstallerResource() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddGolangApp("golang", "../../examples/golang/gin-api").WithGoModTidy(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var golangResource = Assert.Single(appModel.Resources.OfType()); + var installerResource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("golang-go-mod-tidy", installerResource.Name); + Assert.Equal("go", installerResource.Command); + } + + [Fact] + public async Task GolangAppWithGoModTidyHasCorrectArgsAsync() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddGolangApp("golang", "../../examples/golang/gin-api").WithGoModTidy(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var installerResource = Assert.Single(appModel.Resources.OfType()); + + var args = await installerResource.GetArgumentValuesAsync(); + Assert.Collection( + args, + arg => Assert.Equal("mod", arg), + arg => Assert.Equal("tidy", arg) + ); + } + + [Fact] + public void GolangAppWithGoModDownloadCreatesInstallerResource() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddGolangApp("golang", "../../examples/golang/gin-api").WithGoModDownload(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var golangResource = Assert.Single(appModel.Resources.OfType()); + var installerResource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("golang-go-mod-download", installerResource.Name); + Assert.Equal("go", installerResource.Command); + } + + [Fact] + public async Task GolangAppWithGoModDownloadHasCorrectArgsAsync() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddGolangApp("golang", "../../examples/golang/gin-api").WithGoModDownload(); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var installerResource = Assert.Single(appModel.Resources.OfType()); + + var args = await installerResource.GetArgumentValuesAsync(); + Assert.Collection( + args, + arg => Assert.Equal("mod", arg), + arg => Assert.Equal("download", arg) + ); + } + + [Fact] + public void WithGoModTidyNullBuilderThrows() + { + IResourceBuilder builder = null!; + + Assert.Throws(() => builder.WithGoModTidy()); + } + + [Fact] + public void WithGoModDownloadNullBuilderThrows() + { + IResourceBuilder builder = null!; + + Assert.Throws(() => builder.WithGoModDownload()); + } } \ No newline at end of file