diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/GoModInstallerResource.cs b/src/CommunityToolkit.Aspire.Hosting.Golang/GoModInstallerResource.cs new file mode 100644 index 000000000..647415617 --- /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 964dc135d..a356552f6 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs @@ -234,4 +234,88 @@ 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. + /// When true (default), automatically runs go mod tidy before the application starts. When false, the installer resource is created but requires explicit start. + /// Optional action to configure the installer resource. + /// A reference to the . + public static IResourceBuilder WithGoModTidy( + this IResourceBuilder builder, + bool install = true, + Action>? configureInstaller = null) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + + // Only create installer resource if in run mode + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + 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(); + + configureInstaller?.Invoke(installerBuilder); + + if (install) + { + // Make the parent resource wait for the installer to complete + builder.WaitForCompletion(installerBuilder); + } + else + { + // Add WithExplicitStart when install is false + installerBuilder.WithExplicitStart(); + } + } + + return builder; + } + + /// + /// Ensures Go module dependencies are downloaded before the application starts using go mod download. + /// + /// The Golang app resource builder. + /// When true (default), automatically runs go mod download before the application starts. When false, the installer resource is created but requires explicit start. + /// Optional action to configure the installer resource. + /// A reference to the . + public static IResourceBuilder WithGoModDownload( + this IResourceBuilder builder, + bool install = true, + Action>? configureInstaller = null) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + + // Only create installer resource if in run mode + if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) + { + 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(); + + configureInstaller?.Invoke(installerBuilder); + + if (install) + { + // Make the parent resource wait for the installer to complete + builder.WaitForCompletion(installerBuilder); + } + else + { + // Add WithExplicitStart when install is false + installerBuilder.WithExplicitStart(); + } + } + + 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 e7de82f24..2e43a3ae0 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md @@ -29,6 +29,61 @@ 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"); +``` + +By default, `WithGoModTidy()` runs `go mod tidy` before the application starts (equivalent to `install: true`). You can set `install: false` to create the installer resource but require explicit start: + +```csharp +var golang = builder.AddGolangApp("golang", "../gin-api") + .WithGoModTidy(install: false) // Installer created but requires explicit start + .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"); +``` + +Similarly, you can control the installer behavior: + +```csharp +var golang = builder.AddGolangApp("golang", "../gin-api") + .WithGoModDownload(install: false) // Installer created but requires explicit start + .WithHttpEndpoint(env: "PORT"); +``` + +When `install` is `true` (default), the installer resource is created and the Go application waits for it to complete before starting. When `install` is `false`, the installer resource is still created but is set to require explicit start, appearing in the Aspire dashboard but not automatically executing. + +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 create installer resources in run mode (when the application is started locally). They do not run when publishing, as 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 e847ca911..4c26908f7 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/ResourceCreationTests.cs @@ -89,4 +89,150 @@ 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); + + // Verify that the Golang app waits for the installer to complete + Assert.True(golangResource.TryGetAnnotationsOfType(out var waitAnnotations)); + Assert.Contains(waitAnnotations, w => w.Resource == installerResource); + } + + [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.GetArgumentListAsync(); + 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); + + // Verify that the Golang app waits for the installer to complete + Assert.True(golangResource.TryGetAnnotationsOfType(out var waitAnnotations)); + Assert.Contains(waitAnnotations, w => w.Resource == installerResource); + } + + [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.GetArgumentListAsync(); + 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()); + } + + [Fact] + public void GolangAppWithGoModTidyInstallFalseCreatesInstallerWithExplicitStart() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddGolangApp("golang", "../../examples/golang/gin-api").WithGoModTidy(install: false); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + Assert.Single(appModel.Resources.OfType()); + var installerResource = Assert.Single(appModel.Resources.OfType()); + + // Installer should be created even with install: false + Assert.Equal("golang-go-mod-tidy", installerResource.Name); + Assert.Equal("go", installerResource.Command); + + // Verify that the installer has ExplicitStartupAnnotation + Assert.True(installerResource.HasAnnotationOfType()); + } + + [Fact] + public void GolangAppWithGoModDownloadInstallFalseCreatesInstallerWithExplicitStart() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddGolangApp("golang", "../../examples/golang/gin-api").WithGoModDownload(install: false); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + Assert.Single(appModel.Resources.OfType()); + var installerResource = Assert.Single(appModel.Resources.OfType()); + + // Installer should be created even with install: false + Assert.Equal("golang-go-mod-download", installerResource.Name); + Assert.Equal("go", installerResource.Command); + + // Verify that the installer has ExplicitStartupAnnotation + Assert.True(installerResource.HasAnnotationOfType()); + } } \ No newline at end of file