// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#pragma warning disable ASPIREEXTENSION001
#pragma warning disable ASPIREDOCKERFILEBUILDER001
#pragma warning disable ASPIREPIPELINES001
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.ApplicationModel.Docker;
using Aspire.Hosting.Go;
using Aspire.Hosting.Pipelines;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Aspire.Hosting;
/// <summary>
/// Provides extension methods for adding Go applications to an <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
public static class GoHostingExtensions
{
/// <summary>
/// Adds a Go application to the application model. The Go toolchain must be available on the PATH.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to add the resource to.</param>
/// <param name="name">The name of the resource.</param>
/// <param name="appDirectory">
/// The path to the directory that acts as both the Go module root (where <c>go.mod</c> lives)
/// and the Docker build context for <c>aspire publish</c>.
/// </param>
/// <param name="packagePath">
/// The Go package to run or build, relative to <paramref name="appDirectory"/>.
/// Defaults to <c>"."</c> (the module root itself).
/// Use a sub-path such as <c>"./cmd/server"</c> when the main package is not at the module root
/// (e.g. <c>api/cmd/server/main.go</c> with <c>api/go.mod</c>).
/// This value is passed to <c>go run</c>, <c>dlv debug</c>, and <c>go build</c> consistently.
/// </param>
/// <param name="buildTags">Optional build tags passed to the compiler via <c>-tags</c> (e.g. <c>"netgo"</c>, <c>"integration"</c>).</param>
/// <param name="ldFlags">Optional linker flags passed via <c>-ldflags</c> (e.g. <c>"-X main.version=1.0.0"</c>).</param>
/// <param name="gcFlags">Optional compiler flags passed via <c>-gcflags</c> (e.g. <c>"all=-N -l"</c> to disable optimisations for Delve).</param>
/// <param name="raceDetector">When <see langword="true"/>, enables the Go race detector by passing <c>-race</c> to <c>go run</c>.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
/// <ats-returns>The resource builder.</ats-returns>
/// <remarks>
/// <para>
/// This method executes the Go application using <c>go run .</c>. The Go toolchain resolves the
/// entry point from the package in <paramref name="appDirectory"/>.
/// </para>
/// <para>
/// Go applications automatically have VS Code debugging support enabled via Delve.
/// Use <see cref="WithModTidy{T}"/>, <see cref="WithModVendor{T}"/>, or <see cref="WithModDownload{T}"/>
/// to manage module dependencies before startup, and <see cref="WithVetTool{T}"/> to run static analysis.
/// Use <see cref="WithAppArgs{T}"/> to pass runtime program arguments, and
/// <see cref="WithDelveServer{T}(IResourceBuilder{T}, DelveServerOptions)"/> to enable remote debugging via a headless Delve server.
/// </para>
/// </remarks>
/// <example>
/// Add a Go API to the application model with build tags and linker flags:
/// <code lang="csharp">
/// var builder = DistributedApplication.CreateBuilder(args);
///
/// builder.AddGoApp("api", "../go-api",
/// buildTags: ["netgo"],
/// ldFlags: "-X main.version=1.0.0")
/// .WithHttpEndpoint(port: 8080)
/// .WithExternalHttpEndpoints();
///
/// builder.Build().Run();
/// </code>
/// </example>
[AspireExport]
public static IResourceBuilder<GoAppResource> AddGoApp(
this IDistributedApplicationBuilder builder,
[ResourceName] string name,
string appDirectory,
string packagePath = ".",
string[]? buildTags = null,
string? ldFlags = null,
string? gcFlags = null,
bool raceDetector = false)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(name);
ArgumentException.ThrowIfNullOrEmpty(packagePath);
ArgumentException.ThrowIfNullOrEmpty(appDirectory);
appDirectory = Path.GetFullPath(appDirectory, builder.AppHostDirectory);
var resource = new GoAppResource(name, appDirectory);
var rb = builder.AddResource(resource)
.WithIconName("Code")
.WithArgs(ctx =>
{
var programArgs = ctx.Resource.TryGetLastAnnotation<GoAppArgsAnnotation>(out var argsAnnotation)
? argsAnnotation.Args
: [];
if (!ctx.Resource.TryGetLastAnnotation<GoDelveServerAnnotation>(out var delveAnnotation))
{
// Normal run mode. The `go run [build flags] <pkg>` prefix is contributed as entrypoint
// arguments by WithVSCodeDebugging(), so only the program's own arguments belong here.
foreach (var arg in programArgs)
{
ctx.Args.Add(arg);
}
return;
}
// Delve debug mode — global flags MUST precede the subcommand per the Delve CLI:
// dlv --headless=true --listen=127.0.0.1:PORT --api-version=2 debug [--continue] [--build-flags=...] <pkg> [-- args]
// See: https://www.jetbrains.com/help/go/attach-to-running-go-processes-with-debugger.html
// WithDelveServer removes the debug launch annotation, so this whole command line is a plain
// process invocation and stays in the regular argument callback.
var pkg = ctx.Resource.TryGetLastAnnotation<GoPackagePathAnnotation>(out var pkgAnnotation)
? pkgAnnotation.PackagePath
: ".";
ctx.Args.Add("--headless=true");
ctx.Args.Add($"--listen=127.0.0.1:{delveAnnotation.Port}");
ctx.Args.Add("--api-version=2");
if (delveAnnotation.AcceptMultiClient)
{
ctx.Args.Add("--accept-multiclient");
}
if (delveAnnotation.OnlySameUser.HasValue)
{
ctx.Args.Add($"--only-same-user={delveAnnotation.OnlySameUser.Value.ToString().ToLowerInvariant()}");
}
if (delveAnnotation.Log)
{
ctx.Args.Add("--log");
if (!string.IsNullOrEmpty(delveAnnotation.LogOutput))
{
ctx.Args.Add($"--log-output={delveAnnotation.LogOutput}");
}
}
ctx.Args.Add("debug");
if (delveAnnotation.ContinueOnStart)
{
ctx.Args.Add("--continue");
}
var delveBuildFlags = BuildFlagsString(ctx.Resource);
if (delveBuildFlags.Length > 0)
{
ctx.Args.Add($"--build-flags={delveBuildFlags}");
}
ctx.Args.Add(pkg);
if (programArgs.Length > 0)
{
ctx.Args.Add("--");
foreach (var arg in programArgs)
{
ctx.Args.Add(arg);
}
}
})
.WithRequiredCommand("go", "https://go.dev/dl/")
.WithOtlpExporter()
.WithVSCodeDebugging()
.PublishAsDockerFile(containerBuilder =>
{
if (File.Exists(Path.Combine(appDirectory, "Dockerfile")))
{
return;
}
containerBuilder.WithDockerfileBuilder(appDirectory, ctx =>
{
var logger = ctx.Services.GetService<ILogger<GoAppResource>>();
var goVersion = GoVersionDetector.Detect(appDirectory);
ctx.Resource.TryGetLastAnnotation<DockerfileBaseImageAnnotation>(out var baseImageAnnotation);
var buildImage = baseImageAnnotation?.BuildImage ?? $"golang:{goVersion}-alpine";
var runtimeImage = baseImageAnnotation?.RuntimeImage ?? "alpine:latest";
// packagePath comes from the AddGoApp closure — ctx.Resource is the
// ContainerResource created by PublishAsDockerFile and does not carry
// the GoPackagePathAnnotation from the original GoAppResource.
var binaryName = ctx.Resource.Name;
var buildCmd = BuildDockerGoCommand(ctx.Resource, packagePath, binaryName);
var hasGoMod = File.Exists(Path.Combine(appDirectory, "go.mod"));
var hasGoSum = File.Exists(Path.Combine(appDirectory, "go.sum"));
var hasPrivate = ctx.Resource.TryGetLastAnnotation<GoPrivateAnnotation>(out var privateAnnotation);
var buildStage = ctx.Builder
.From(buildImage, "build")
.WorkDir("/app")
// CGO_ENABLED=0 produces a fully static binary; GOOS=linux ensures the
// correct target even when building on macOS or Windows hosts.
.Env("CGO_ENABLED", "0")
.Env("GOOS", "linux");
if (hasPrivate)
{
// ARG carries the non-sensitive username; the token comes via --mount=type=secret.
buildStage.Arg(privateAnnotation!.UsernameArgName);
// GOPRIVATE implicitly sets GONOSUMCHECK and GONOPROXY for the listed paths,
// so the toolchain fetches them directly rather than going through the public proxy.
buildStage.Env("GOPRIVATE", string.Join(",", privateAnnotation.PrivatePatterns));
// git is required for private module fetching over HTTPS.
if (buildImage.Contains("alpine", StringComparison.OrdinalIgnoreCase))
{
buildStage.Run("apk add --no-cache git");
}
}
if (hasGoMod)
{
buildStage.Copy("go.mod", "./");
if (hasGoSum)
{
buildStage.Copy("go.sum", "./");
}
if (hasPrivate)
{
// Write .netrc from ARG + secret, download modules, then remove .netrc
// — all in one layer so credentials never persist in the image.
var usernameRef = "${" + privateAnnotation!.UsernameArgName + "}";
var downloadCmd = string.Join(" && ",
$"GH_TOKEN=$(cat /run/secrets/{privateAnnotation.TokenSecretId})",
$"echo \"machine {privateAnnotation.AuthHost} login {usernameRef} password ${{GH_TOKEN}}\" >> $HOME/.netrc",
"go mod download",
"rm -f $HOME/.netrc");
buildStage.RunWithMounts(
downloadCmd,
"type=cache,target=/root/go/pkg/mod",
$"type=secret,id={privateAnnotation.TokenSecretId}");
}
else
{
// Cache the module download so repeated builds don't re-fetch the internet.
buildStage.RunWithMounts(
"go mod download",
"type=cache,target=/root/go/pkg/mod");
}
}
buildStage
.Copy(".", ".")
// Cache the Go build cache and the module cache across builds for fast
// incremental compilation inside Docker.
.RunWithMounts(
buildCmd,
"type=cache,target=/root/.cache/go-build",
"type=cache,target=/root/go/pkg/mod");
// Add intermediate FROM stages for any container files sources
// (e.g. FROM frontend AS frontend_stage).
ctx.Builder.AddContainerFilesStages(ctx.Resource, logger);
var runtimeStage = ctx.Builder.From(runtimeImage);
// Only use apk when the runtime image is Alpine-based.
// For custom images (e.g. debian:bookworm-slim) the caller
// is expected to supply a base image that already includes
// ca-certificates and tzdata, or extend the Dockerfile.
if (runtimeImage.Contains("alpine", StringComparison.OrdinalIgnoreCase))
{
runtimeStage
.Run("apk --no-cache add ca-certificates tzdata")
// Create a non-root user — Alpine uses addgroup/adduser.
.Run("addgroup -S app && adduser -S -G app app");
}
else
{
// Debian/Ubuntu and other glibc-based images use groupadd/useradd.
runtimeStage.Run("groupadd --system --gid 999 app && useradd --system --gid 999 --uid 999 --no-create-home app");
}
runtimeStage
.WorkDir("/app")
// Add COPY --from=<source> instructions for each container files source.
.AddContainerFiles(ctx.Resource, "/app", logger)
.CopyFrom("build", $"/app/{ctx.Resource.Name}", $"/app/{ctx.Resource.Name}")
.User("app")
.Entrypoint([$"/app/{ctx.Resource.Name}"]);
});
});
if (packagePath != ".")
{
rb.WithAnnotation(new GoPackagePathAnnotation(packagePath), ResourceAnnotationMutationBehavior.Replace);
}
if (buildTags is { Length: > 0 })
{
rb.WithAnnotation(new GoBuildTagsAnnotation(buildTags), ResourceAnnotationMutationBehavior.Replace);
}
if (ldFlags is not null)
{
rb.WithAnnotation(new GoLdFlagsAnnotation(ldFlags), ResourceAnnotationMutationBehavior.Replace);
}
if (gcFlags is not null)
{
rb.WithAnnotation(new GoGcFlagsAnnotation(gcFlags), ResourceAnnotationMutationBehavior.Replace);
}
if (raceDetector)
{
rb.WithAnnotation(new GoRaceDetectorAnnotation(), ResourceAnnotationMutationBehavior.Replace);
}
// Ensure source resources (that provide container files) build before this Go image.
rb.WithPipelineConfiguration(context =>
{
if (rb.Resource.TryGetAnnotationsOfType<ContainerFilesDestinationAnnotation>(
out var containerFilesAnnotations))
{
var buildSteps = context.GetSteps(rb.Resource, WellKnownPipelineTags.BuildCompute);
foreach (var containerFile in containerFilesAnnotations)
{
buildSteps.DependsOn(context.GetSteps(containerFile.Source, WellKnownPipelineTags.BuildCompute));
}
}
});
return rb;
}
/// <summary>
/// Passes extra arguments to the Go program at runtime.
/// In normal run mode they appear after <c>go run .</c>; in Delve mode after the <c>--</c> separator.
/// </summary>
/// <typeparam name="T">The type of the Go application resource.</typeparam>
/// <param name="builder">The resource builder for the Go application.</param>
/// <param name="args">The program arguments (e.g., <c>"serve"</c>, <c>"--config"</c>, <c>"prod.yaml"</c>).</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <ats-returns>The resource builder.</ats-returns>
[AspireExport]
public static IResourceBuilder<T> WithAppArgs<T>(this IResourceBuilder<T> builder, params object[] args)
where T : GoAppResource
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(args);
return builder.WithAnnotation(new GoAppArgsAnnotation(args), ResourceAnnotationMutationBehavior.Replace);
}
/// <summary>
/// Runs <c>go mod tidy</c> before starting the application, ensuring <c>go.sum</c> is up to date.
/// The main application waits for the tidy step to complete successfully before launching.
/// </summary>
/// <typeparam name="T">The type of the Go application resource.</typeparam>
/// <param name="builder">The resource builder for the Go application.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <ats-returns>The resource builder.</ats-returns>
[AspireExport]
public static IResourceBuilder<T> WithModTidy<T>(this IResourceBuilder<T> builder)
where T : GoAppResource
{
ArgumentNullException.ThrowIfNull(builder);
// Guard against duplicate resource creation if called more than once.
if (builder.Resource.TryGetLastAnnotation<GoModTidyAnnotation>(out _))
{
return builder;
}
builder.WithAnnotation(new GoModTidyAnnotation());
// Only create the setup resource in run mode; it has no meaning during publish.
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
var tidyResource = new ExecutableResource(
$"{builder.Resource.Name}-mod-tidy", "go", builder.Resource.WorkingDirectory);
var tidy = builder.ApplicationBuilder
.AddResource(tidyResource)
.WithArgs("mod", "tidy", "-e")
.ExcludeFromManifest();
// Store the builder reference so WithModVendor/WithModDownload can chain after tidy.
builder.WithAnnotation(new GoModTidyBuilderAnnotation(tidy));
// If WithModVendor was called before WithModTidy (reverse order), retroactively
// make the vendor sibling wait for tidy so the ordering is correct regardless of
// which With* method the caller invokes first.
if (builder.Resource.TryGetLastAnnotation<GoModVendorBuilderAnnotation>(out var existingVendor))
{
existingVendor.Sibling.WaitForCompletion(tidy);
}
builder.WaitForCompletion(tidy);
}
return builder;
}
/// <summary>
/// Runs <c>go mod vendor</c> before starting the application, caching all module dependencies
/// in the local <c>vendor/</c> directory.
/// The main application waits for the vendor step to complete successfully before launching.
/// </summary>
/// <typeparam name="T">The type of the Go application resource.</typeparam>
/// <param name="builder">The resource builder for the Go application.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <ats-returns>The resource builder.</ats-returns>
[AspireExport]
public static IResourceBuilder<T> WithModVendor<T>(this IResourceBuilder<T> builder)
where T : GoAppResource
{
ArgumentNullException.ThrowIfNull(builder);
if (builder.Resource.TryGetLastAnnotation<GoModVendorAnnotation>(out _))
{
return builder;
}
builder.WithAnnotation(new GoModVendorAnnotation());
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
var vendorResource = new ExecutableResource(
$"{builder.Resource.Name}-mod-vendor", "go", builder.Resource.WorkingDirectory);
var vendor = builder.ApplicationBuilder
.AddResource(vendorResource)
.WithArgs("mod", "vendor")
.ExcludeFromManifest();
// vendor must run after tidy: tidy updates go.mod/go.sum which vendor reads.
if (builder.Resource.TryGetLastAnnotation<GoModTidyBuilderAnnotation>(out var tidyBuilder))
{
vendor.WaitForCompletion(tidyBuilder.Sibling);
}
// Store vendor builder so WithModDownload can chain after it.
builder.WithAnnotation(new GoModVendorBuilderAnnotation(vendor));
builder.WaitForCompletion(vendor);
}
return builder;
}
/// <summary>
/// Runs <c>go mod download</c> before starting the application, pre-fetching all module
/// dependencies into the local module cache without modifying <c>go.sum</c>.
/// The main application waits for the download step to complete successfully before launching.
/// </summary>
/// <typeparam name="T">The type of the Go application resource.</typeparam>
/// <param name="builder">The resource builder for the Go application.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <ats-returns>The resource builder.</ats-returns>
[AspireExport]
public static IResourceBuilder<T> WithModDownload<T>(this IResourceBuilder<T> builder)
where T : GoAppResource
{
ArgumentNullException.ThrowIfNull(builder);
if (builder.Resource.TryGetLastAnnotation<GoModDownloadAnnotation>(out _))
{
return builder;
}
builder.WithAnnotation(new GoModDownloadAnnotation());
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
var downloadResource = new ExecutableResource(
$"{builder.Resource.Name}-mod-download", "go", builder.Resource.WorkingDirectory);
var download = builder.ApplicationBuilder
.AddResource(downloadResource)
.WithArgs("mod", "download")
.ExcludeFromManifest();
// download must run after tidy (tidy may add/remove entries) and after
// vendor (vendor and download both populate module state — run sequentially).
if (builder.Resource.TryGetLastAnnotation<GoModVendorBuilderAnnotation>(out var vendorBuilder))
{
download.WaitForCompletion(vendorBuilder.Sibling);
}
else if (builder.Resource.TryGetLastAnnotation<GoModTidyBuilderAnnotation>(out var tidyBuilder))
{
download.WaitForCompletion(tidyBuilder.Sibling);
}
builder.WaitForCompletion(download);
}
return builder;
}
/// <summary>
/// Runs <c>go vet ./...</c> before starting the application to catch static analysis issues.
/// The main application waits for the vet step to complete successfully before launching.
/// </summary>
/// <typeparam name="T">The type of the Go application resource.</typeparam>
/// <param name="builder">The resource builder for the Go application.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <ats-returns>The resource builder.</ats-returns>
[AspireExport]
public static IResourceBuilder<T> WithVetTool<T>(this IResourceBuilder<T> builder)
where T : GoAppResource
{
ArgumentNullException.ThrowIfNull(builder);
if (builder.Resource.TryGetLastAnnotation<GoVetToolAnnotation>(out _))
{
return builder;
}
builder.WithAnnotation(new GoVetToolAnnotation());
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
var vetResource = new ExecutableResource(
$"{builder.Resource.Name}-vet-tool", "go", builder.Resource.WorkingDirectory);
var vet = builder.ApplicationBuilder
.AddResource(vetResource)
.WithArgs("vet", "./...")
.ExcludeFromManifest();
builder.WaitForCompletion(vet);
}
return builder;
}
/// <summary>
/// Configures private Go module authentication for publish-time Dockerfile generation.
/// </summary>
/// <typeparam name="T">The type of the Go application resource.</typeparam>
/// <param name="builder">The resource builder for the Go application.</param>
/// <param name="privatePatterns">
/// One or more module path patterns that should bypass the public proxy and checksum database,
/// e.g. <c>"*.mycompany.com"</c> or <c>"github.com/myorg"</c>.
/// Passed verbatim to <c>GOPRIVATE</c>, which implicitly covers <c>GONOSUMCHECK</c> and <c>GONOPROXY</c>.
/// </param>
/// <param name="authHost">The Git host that requires authentication, e.g. <c>"github.com"</c>.</param>
/// <param name="usernameArgName">
/// The Docker build-arg name for the Git username. Defaults to <c>"GIT_USER"</c>.
/// Pass it at build time with <c>--build-arg GIT_USER=myuser</c>.
/// </param>
/// <param name="tokenSecretId">
/// The BuildKit secret ID for the Git access token. Defaults to <c>"gittoken"</c>.
/// Pass it at build time with <c>--secret id=gittoken,src=/path/to/token</c>.
/// </param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <ats-returns>The resource builder.</ats-returns>
/// <remarks>
/// <para>
/// Only affects the generated Dockerfile — has no effect in run mode, where the local
/// Go toolchain picks up credentials from the developer's own <c>~/.netrc</c> or git
/// credential helper.
/// </para>
/// <para>
/// The generated build stage writes a temporary <c>.netrc</c> file from the username
/// build-arg and the token secret, runs <c>go mod download</c>, then removes the file —
/// all in a single layer so credentials never persist in the image.
/// </para>
/// </remarks>
/// <example>
/// <code lang="csharp">
/// builder.AddGoApp("api", "../go-api")
/// .WithGoPrivate(["github.com/myorg"], "github.com");
/// </code>
/// Build with:
/// <code lang="sh">
/// docker build --build-arg GIT_USER=myuser --secret id=gittoken,src=~/.git-token .
/// </code>
/// </example>
[AspireExport]
public static IResourceBuilder<T> WithGoPrivate<T>(
this IResourceBuilder<T> builder,
string[] privatePatterns,
string authHost,
string usernameArgName = "GIT_USER",
string tokenSecretId = "gittoken")
where T : GoAppResource
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(privatePatterns);
ArgumentException.ThrowIfNullOrEmpty(authHost);
ArgumentException.ThrowIfNullOrEmpty(usernameArgName);
ArgumentException.ThrowIfNullOrEmpty(tokenSecretId);
return builder.WithAnnotation(
new GoPrivateAnnotation
{
PrivatePatterns = privatePatterns,
AuthHost = authHost,
UsernameArgName = usernameArgName,
TokenSecretId = tokenSecretId,
},
ResourceAnnotationMutationBehavior.Replace);
}
/// <summary>
/// Starts a headless Delve debug server so that a DAP-compatible client can attach remotely.
/// The application is launched with <c>dlv debug</c> instead of <c>go run</c>.
/// Delve must be available on the PATH.
/// </summary>
/// <typeparam name="T">The type of the Go application resource.</typeparam>
/// <param name="builder">The resource builder for the Go application.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <remarks>
/// <para>
/// Delve is the only Go debugger; both GoLand and VS Code use it under the hood, just in
/// different modes:
/// </para>
/// <list type="bullet">
/// <item>
/// <term>GoLand</term>
/// <description>Create a <em>Go Remote</em> run configuration pointing at
/// <c>localhost:<port></c> and start it after the resource has started.</description>
/// </item>
/// <item>
/// <term>VS Code (attach mode)</term>
/// <description>Add a <c>"request": "attach"</c> entry to <c>launch.json</c> with
/// <c>"mode": "remote"</c>, <c>"host": "localhost"</c>, and <c>"port": <port></c>,
/// then start it after the resource has started.</description>
/// </item>
/// </list>
/// <para>
/// VS Code users who do not need GoLand compatibility can rely on the automatic VS Code
/// debugging support that <see cref="AddGoApp"/> enables by default — no change to the
/// application command is required in that case.
/// </para>
/// </remarks>
/// <example>
/// <code lang="csharp">
/// builder.AddGoApp("api", "../go-api")
/// .WithDelveServer();
/// </code>
/// </example>
[AspireExportIgnore(Reason = "This C# convenience overload uses default options. Polyglot AppHosts use the DelveServerOptions overload.")]
public static IResourceBuilder<T> WithDelveServer<T>(this IResourceBuilder<T> builder)
where T : GoAppResource
=> builder.WithDelveServer(new DelveServerOptions());
/// <summary>
/// Starts a headless Delve debug server on the specified port.
/// </summary>
/// <typeparam name="T">The type of the Go application resource.</typeparam>
/// <param name="builder">The resource builder for the Go application.</param>
/// <param name="port">The TCP port Delve listens on. Defaults to <c>2345</c>.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <remarks>
/// This overload is retained for binary compatibility. Use <see cref="WithDelveServer{T}(IResourceBuilder{T})"/>
/// for the default port or <see cref="WithDelveServer{T}(IResourceBuilder{T}, DelveServerOptions)"/>
/// to configure the port and other Delve server options.
/// </remarks>
/// <example>
/// <code lang="csharp">
/// builder.AddGoApp("api", "../go-api")
/// .WithDelveServer(new DelveServerOptions { Port = 3456 });
/// </code>
/// </example>
[Obsolete("Use WithDelveServer() or WithDelveServer(DelveServerOptions) instead.")]
[AspireExportIgnore(Reason = "This obsolete compatibility overload is C#-only. Polyglot AppHosts use the DelveServerOptions overload.")]
public static IResourceBuilder<T> WithDelveServer<T>(this IResourceBuilder<T> builder, int port = 2345)
where T : GoAppResource
=> builder.WithDelveServer(new DelveServerOptions { Port = port });
/// <summary>
/// Starts a configurable headless Delve debug server so that DAP-compatible clients can attach remotely.
/// The application is launched with <c>dlv debug</c> instead of <c>go run</c>.
/// Delve must be available on the PATH.
/// </summary>
/// <typeparam name="T">The type of the Go application resource.</typeparam>
/// <param name="builder">The resource builder for the Go application.</param>
/// <param name="options">The options that configure the Delve server. When <see langword="null"/>, the default options are used.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <ats-returns>The resource builder.</ats-returns>
/// <remarks>
/// The server listens on <c>127.0.0.1</c> and accepts a single debugger client by default.
/// Set <see cref="DelveServerOptions.AcceptMultiClient"/> to <see langword="true"/> only when
/// multiple clients or reconnections are required.
/// </remarks>
/// <example>
/// <code lang="csharp">
/// builder.AddGoApp("api", "../go-api")
/// .WithDelveServer(new DelveServerOptions
/// {
/// Port = 2345,
/// ContinueOnStart = true,
/// Log = true,
/// LogOutput = "rpc,dap,debugger"
/// });
/// </code>
/// </example>
[AspireExport]
public static IResourceBuilder<T> WithDelveServer<T>(
this IResourceBuilder<T> builder,
DelveServerOptions? options = null)
where T : GoAppResource
{
ArgumentNullException.ThrowIfNull(builder);
options ??= new DelveServerOptions();
// WithDelveServer changes the resource into a headless Delve process that IDEs attach to
// manually. Leaving the VS Code launch annotation in place would make DCP hand execution to
// the IDE instead of starting that Delve server.
var debuggingAnnotation = builder.Resource.Annotations.OfType<SupportsDebuggingAnnotation>().FirstOrDefault();
if (debuggingAnnotation is not null)
{
builder.Resource.Annotations.Remove(debuggingAnnotation);
}
// Switch the underlying executable from "go" to "dlv" using Replace so that
// calling WithDelveServer more than once is idempotent.
return builder
.WithAnnotation(
new ExecutableAnnotation { Command = "dlv", WorkingDirectory = builder.Resource.WorkingDirectory },
ResourceAnnotationMutationBehavior.Replace)
.WithAnnotation(
new GoDelveServerAnnotation(
options.Port,
options.AcceptMultiClient,
options.OnlySameUser,
options.ContinueOnStart,
options.Log,
options.LogOutput),
ResourceAnnotationMutationBehavior.Replace)
.WithRequiredCommand("dlv", "https://github.com/go-delve/delve");
}
[System.Diagnostics.CodeAnalysis.Experimental("ASPIREEXTENSION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
internal static IResourceBuilder<T> WithVSCodeDebugging<T>(this IResourceBuilder<T> builder)
where T : GoAppResource
{
ArgumentNullException.ThrowIfNull(builder);
var resource = builder.Resource;
return builder.WithDebugSupport(
mode =>
{
// Resolve annotations when DCP creates the launch configuration so later
// resource mutations such as WithWorkingDirectory(...) are reflected.
var workingDirectory = Path.GetFullPath(resource.WorkingDirectory);
var packagePath = resource.TryGetLastAnnotation<GoPackagePathAnnotation>(out var packagePathAnnotation)
? packagePathAnnotation.PackagePath
: ".";
var buildFlags = BuildFlagsString(resource);
return new GoLaunchConfiguration
{
Program = Path.GetFullPath(packagePath, workingDirectory),
Mode = mode,
WorkingDirectory = workingDirectory,
BuildFlags = buildFlags.Length > 0 ? buildFlags : null
};
},
"go")
.WithLaunchToolArgs(static ctx =>
{
// The executable resource normally starts as:
// go run [-race] [-tags=...] [-ldflags=...] [-gcflags=...] <pkg> [app args]
// Everything up to and including <pkg> is the tool invocation: in IDE mode VS Code's Go debugger
// performs it via program/buildFlags, so it is not passed to the launched program.
if (ctx.Resource.HasAnnotationOfType<GoDelveServerAnnotation>())
{
// WithDelveServer replaces the whole command line with a headless `dlv debug ...` invocation and
// removes the debug launch annotation, so there is no `go run` prefix to contribute.
return;
}
ctx.Args.Add("run");
if (ctx.Resource.TryGetLastAnnotation<GoRaceDetectorAnnotation>(out _))
{
ctx.Args.Add("-race");
}
if (ctx.Resource.TryGetLastAnnotation<GoBuildTagsAnnotation>(out var tagsAnnotation))
{
ctx.Args.Add($"-tags={string.Join(",", tagsAnnotation.Tags)}");
}
if (ctx.Resource.TryGetLastAnnotation<GoLdFlagsAnnotation>(out var ldFlagsAnnotation))
{
ctx.Args.Add($"-ldflags={ldFlagsAnnotation.Flags}");
}
if (ctx.Resource.TryGetLastAnnotation<GoGcFlagsAnnotation>(out var gcFlagsAnnotation))
{
ctx.Args.Add($"-gcflags={gcFlagsAnnotation.Flags}");
}
ctx.Args.Add(ctx.Resource.TryGetLastAnnotation<GoPackagePathAnnotation>(out var pkgAnnotation)
? pkgAnnotation.PackagePath
: ".");
},
ownedByLaunchConfigurationType: "go");
}
/// <summary>
/// Builds the <c>go build</c> command for the generated Dockerfile, propagating any
/// build-time flags that were set on the resource via <see cref="AddGoApp"/>.
/// </summary>
private static string BuildDockerGoCommand(IResource resource, string packagePath = ".", string binaryName = "app")
{
var parts = new List<string> { "go", "build" };
// Race detection requires CGO; the Dockerfile sets CGO_ENABLED=0 for a fully static
// binary, so -race is intentionally excluded from publish/deploy builds.
parts.AddRange(BuildFlagParts(resource, includeRace: false));
parts.AddRange(["-o", $"/app/{binaryName}", packagePath]);
return string.Join(" ", parts);
}
/// <summary>
/// Builds the combined build-flags string from present annotations.
/// Returns an empty string when no flags are set.
/// For <c>go run</c> the flags are individual args; for <c>dlv --build-flags</c> they are combined.
/// </summary>
private static string BuildFlagsString(IResource resource) =>
string.Join(" ", BuildFlagParts(resource, includeRace: true));
/// <summary>
/// Returns the ordered flag tokens derived from build annotations on the resource.
/// Shared by <see cref="BuildDockerGoCommand"/> and <see cref="BuildFlagsString"/> so
/// that flag ordering and quoting rules are defined in exactly one place.
/// </summary>
private static List<string> BuildFlagParts(IResource resource, bool includeRace = true)
{
var parts = new List<string>();
if (includeRace && resource.TryGetLastAnnotation<GoRaceDetectorAnnotation>(out _))
{
parts.Add("-race");
}
if (resource.TryGetLastAnnotation<GoBuildTagsAnnotation>(out var tagsAnnotation))
{
parts.Add($"-tags={ShellQuote(string.Join(",", tagsAnnotation.Tags))}");
}
if (resource.TryGetLastAnnotation<GoLdFlagsAnnotation>(out var ldFlagsAnnotation))
{
parts.Add($"-ldflags={ShellQuote(ldFlagsAnnotation.Flags)}");
}
if (resource.TryGetLastAnnotation<GoGcFlagsAnnotation>(out var gcFlagsAnnotation))
{
parts.Add($"-gcflags={ShellQuote(gcFlagsAnnotation.Flags)}");
}
return parts;
}
/// <summary>
/// Wraps <paramref name="value"/> in POSIX single quotes so that all shell
/// metacharacters (<c>$</c>, <c>`</c>, <c>\</c>, <c>"</c>, <c>;</c>,
/// <c>&</c>, <c>|</c>, space, etc.) are treated as literals in Dockerfile
/// <c>RUN</c> shell-form commands and Delve's <c>--build-flags</c> parser.
/// Embedded single quotes are escaped with the standard POSIX technique:
/// <c>'</c> → <c>'\''</c>.
/// </summary>
private static string ShellQuote(string value) =>
$"'{value.Replace("'", "'\\''")}'";
}