Skip to content
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,9 @@
namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// A resource that represents a Go module installer that runs go mod tidy or go mod download.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="workingDirectory">The working directory to use for the command.</param>
public class GoModInstallerResource(string name, string workingDirectory)
: ExecutableResource(name, "go", workingDirectory);
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,88 @@ private static string GetDefaultGoBaseImage(string workingDirectory, IServicePro
logger.LogDebug("No Go version detected, will use default version");
return null;
}

/// <summary>
/// Ensures Go module dependencies are tidied before the application starts using <c>go mod tidy</c>.
/// </summary>
/// <param name="builder">The Golang app resource builder.</param>
/// <param name="install">When true (default), automatically runs go mod tidy before the application starts. When false, the installer resource is created but requires explicit start.</param>
/// <param name="configureInstaller">Optional action to configure the installer resource.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<GolangAppExecutableResource> WithGoModTidy(
this IResourceBuilder<GolangAppExecutableResource> builder,
bool install = true,
Action<IResourceBuilder<GoModInstallerResource>>? 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;
}

/// <summary>
/// Ensures Go module dependencies are downloaded before the application starts using <c>go mod download</c>.
/// </summary>
/// <param name="builder">The Golang app resource builder.</param>
/// <param name="install">When true (default), automatically runs go mod download before the application starts. When false, the installer resource is created but requires explicit start.</param>
/// <param name="configureInstaller">Optional action to configure the installer resource.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<GolangAppExecutableResource> WithGoModDownload(
this IResourceBuilder<GolangAppExecutableResource> builder,
bool install = true,
Action<IResourceBuilder<GoModInstallerResource>>? 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;
}
Comment thread
tommasodotNET marked this conversation as resolved.
}
55 changes: 55 additions & 0 deletions src/CommunityToolkit.Aspire.Hosting.Golang/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DistributedApplicationModel>();

var golangResource = Assert.Single(appModel.Resources.OfType<GolangAppExecutableResource>());
var installerResource = Assert.Single(appModel.Resources.OfType<GoModInstallerResource>());

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<WaitAnnotation>(out var waitAnnotations));
Assert.Contains(waitAnnotations, w => w.Resource == installerResource);
}
Comment thread
tommasodotNET marked this conversation as resolved.

[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<DistributedApplicationModel>();

var installerResource = Assert.Single(appModel.Resources.OfType<GoModInstallerResource>());

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<DistributedApplicationModel>();

var golangResource = Assert.Single(appModel.Resources.OfType<GolangAppExecutableResource>());
var installerResource = Assert.Single(appModel.Resources.OfType<GoModInstallerResource>());

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<WaitAnnotation>(out var waitAnnotations));
Assert.Contains(waitAnnotations, w => w.Resource == installerResource);
}
Comment thread
tommasodotNET marked this conversation as resolved.

[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<DistributedApplicationModel>();

var installerResource = Assert.Single(appModel.Resources.OfType<GoModInstallerResource>());

var args = await installerResource.GetArgumentListAsync();
Assert.Collection(
args,
arg => Assert.Equal("mod", arg),
arg => Assert.Equal("download", arg)
);
}

[Fact]
public void WithGoModTidyNullBuilderThrows()
{
IResourceBuilder<GolangAppExecutableResource> builder = null!;

Assert.Throws<ArgumentNullException>(() => builder.WithGoModTidy());
}

[Fact]
public void WithGoModDownloadNullBuilderThrows()
{
IResourceBuilder<GolangAppExecutableResource> builder = null!;

Assert.Throws<ArgumentNullException>(() => 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<DistributedApplicationModel>();

Assert.Single(appModel.Resources.OfType<GolangAppExecutableResource>());
var installerResource = Assert.Single(appModel.Resources.OfType<GoModInstallerResource>());

// 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<ExplicitStartupAnnotation>());
}

[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<DistributedApplicationModel>();

Assert.Single(appModel.Resources.OfType<GolangAppExecutableResource>());
var installerResource = Assert.Single(appModel.Resources.OfType<GoModInstallerResource>());

// 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<ExplicitStartupAnnotation>());
}
}
Loading