diff --git a/examples/golang/CommunityToolkit.Aspire.Hosting.Golang.AppHost/Program.cs b/examples/golang/CommunityToolkit.Aspire.Hosting.Golang.AppHost/Program.cs index 574fabbb1..3ec59a6d5 100644 --- a/examples/golang/CommunityToolkit.Aspire.Hosting.Golang.AppHost/Program.cs +++ b/examples/golang/CommunityToolkit.Aspire.Hosting.Golang.AppHost/Program.cs @@ -4,4 +4,14 @@ .WithHttpEndpoint(env: "PORT") .WithHttpHealthCheck("/health"); +// Example: Copy static frontend files into the Golang container when publishing +// This demonstrates the IContainerFilesDestinationResource support +// Uncomment the following line to copy files from a static directory +// (relative to the AppHost project directory, not the Golang app directory): +// golang.WithContainerFiles("/app/static", "../static-frontend"); + +// Or, when using a frontend build resource (e.g., Vite, React, etc.): +// var frontend = builder.AddViteApp("frontend", "../frontend"); +// golang.WithContainerFiles("/app/static", frontend.Resource); + builder.Build().Run(); \ No newline at end of file diff --git a/examples/golang/gin-api/main.go b/examples/golang/gin-api/main.go index 1d5fd69ea..00bfabe47 100644 --- a/examples/golang/gin-api/main.go +++ b/examples/golang/gin-api/main.go @@ -39,6 +39,12 @@ func setupRouter() *gin.Engine { c.String(http.StatusOK, "ok") }) + // Serve static files from the /app/static directory inside the container. + // When using the container files feature, these files are copied from + // a frontend resource into the container at /app/static (see AddContainerFiles and WORKDIR /app). + // If you change the working directory or copy location, update this path accordingly. + r.Static("/static", "/app/static") + // Ping test r.GET("/ping", func(c *gin.Context) { // _, span := tracer.Start(c.Request.Context(), "ping") diff --git a/examples/golang/static-frontend/README.md b/examples/golang/static-frontend/README.md new file mode 100644 index 000000000..064c82041 --- /dev/null +++ b/examples/golang/static-frontend/README.md @@ -0,0 +1,39 @@ +# Static Frontend Example + +This directory contains a simple static HTML frontend that demonstrates the container files feature of the Golang integration. + +## Purpose + +When publishing an Aspire application, static files from this directory can be copied into the Golang container using the `WithContainerFiles` method. This is useful for serving frontend assets (HTML, CSS, JavaScript, images, etc.) directly from your Go application. + +## Usage + +In your AppHost `Program.cs`, you can configure the Golang resource to copy these files: + +```csharp +var golang = builder.AddGolangApp("golang", "../gin-api") + .WithHttpEndpoint(env: "PORT") + .WithContainerFiles("/app/static", "../static-frontend"); +``` + +The Golang application is configured to serve these files from the `/static` endpoint. When running in a container, the files will be available at `http://localhost:/static/index.html`. + +## Container Files Feature + +The Golang resource implements `IContainerFilesDestinationResource`, which tells the Aspire publishing pipeline where to copy files when building the container image. In this example, `/app/static` is used as the conventional destination (and is returned by the `ContainerFilesDestination` property), but the actual destination is determined by the first parameter you pass to `WithContainerFiles` and can be customized. + +This feature works with any resource that produces output files, including: +- Static file directories (like this example) +- Frontend build tools (Vite, webpack, Create React App, etc.) +- Documentation generators +- Any other build output + +## Implementation Details + +The feature works by: +1. The Golang resource implementing `IContainerFilesDestinationResource` +2. Using `WithContainerFiles` to specify the source and destination +3. During publishing, the Aspire pipeline automatically copies the files into the generated Docker image +4. The Golang application serves the files using Gin's `Static` method + +See the main README in the parent directory for more details. diff --git a/examples/golang/static-frontend/index.html b/examples/golang/static-frontend/index.html new file mode 100644 index 000000000..22bec4a39 --- /dev/null +++ b/examples/golang/static-frontend/index.html @@ -0,0 +1,42 @@ + + + + + + Golang + Static Frontend Example + + + +
+

🚀 Golang + Static Frontend

+

This static HTML page is served from the Golang container!

+

It demonstrates the container files feature, where static frontend files are copied into the Golang container during publishing.

+
+ Status: Container files feature working! ✅ +
+
+ + diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppExecutableResource.cs b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppExecutableResource.cs index 4ebdd3cea..7475b65cd 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppExecutableResource.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppExecutableResource.cs @@ -6,4 +6,8 @@ /// The name of the resource. /// The working directory to use for the command. public class GolangAppExecutableResource(string name, string workingDirectory) - : ExecutableResource(name, "go", workingDirectory), IResourceWithServiceDiscovery; \ No newline at end of file + : ExecutableResource(name, "go", workingDirectory), IResourceWithServiceDiscovery, IContainerFilesDestinationResource +{ + /// + public string ContainerFilesDestination => "/app/static"; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs index a356552f6..0c9b05ca7 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/GolangAppHostingExtension.cs @@ -1,4 +1,5 @@ using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.ApplicationModel.Docker; using CommunityToolkit.Aspire.Utils; using System.Diagnostics; using System.Text.RegularExpressions; @@ -123,12 +124,14 @@ private static IResourceBuilder PublishAsGolangDock .Run(string.Join(" ", ["CGO_ENABLED=0", "go", .. buildArgs])); var runtimeImage = baseImageAnnotation?.RuntimeImage ?? $"alpine:{DefaultAlpineVersion}"; + var logger = context.Services.GetService>() ?? NullLogger.Instance; context.Builder .From(runtimeImage) .Run("apk --no-cache add ca-certificates") .WorkDir("/app") .CopyFrom(buildStage.StageName!, "/build/server", "/app/server") + .AddContainerFiles(context.Resource, "/app", logger) .Entrypoint(["/app/server"]); }); }); diff --git a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md index 2e43a3ae0..bb0f2a44e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Golang/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Golang/README.md @@ -107,6 +107,22 @@ var golang = builder.AddGolangApp("golang", "../gin-api") runtimeImage: "alpine:3.20"); ``` +### Container Files Support + +The Golang resource supports copying files from other resources into the container during publishing. This is useful for serving static frontend files from your Go application. The resource implements `IContainerFilesDestinationResource` with a default destination of `/app/static`. + +To copy files from another resource (such as a frontend build) into your Golang container, use the `WithContainerFiles` method: + +```csharp +var frontend = builder.AddViteApp("frontend", "./frontend"); + +var golang = builder.AddGolangApp("golang", "../gin-api") + .WithHttpEndpoint(env: "PORT") + .WithContainerFiles("/app/static", frontend.Resource); +``` + +This will copy the output files from the `frontend` resource into the `/app/static` directory in the Golang container when publishing. Note: `WithContainerFiles` is provided by the Aspire framework when the resource implements `IContainerFilesDestinationResource` and may require additional using directives (for example, `using Aspire.Hosting;`). + ### Generated Dockerfile The automatically generated Dockerfile: diff --git a/tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/ResourceCreationTests.cs index 4c26908f7..941f58999 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/ResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Golang.Tests/ResourceCreationTests.cs @@ -235,4 +235,45 @@ public void GolangAppWithGoModDownloadInstallFalseCreatesInstallerWithExplicitSt // Verify that the installer has ExplicitStartupAnnotation Assert.True(installerResource.HasAnnotationOfType()); } -} \ No newline at end of file + + [Fact] + public void GolangAppImplementsIContainerFilesDestinationResource() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddGolangApp("golang", "../../examples/golang/gin-api"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + Assert.IsAssignableFrom(resource); + + // Verify the default destination path + Assert.Equal("/app/static", resource.ContainerFilesDestination); + } + + [Fact] + public void GolangAppWithContainerFilesInterfaceSupportsPublishing() + { + var builder = DistributedApplication.CreateBuilder(); + + var golang = builder.AddGolangApp("golang", "../../examples/golang/gin-api"); + + using var app = builder.Build(); + + var appModel = app.Services.GetRequiredService(); + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + + // Verify the resource implements IContainerFilesDestinationResource + Assert.IsAssignableFrom(resource); + + // Verify the default destination path is set correctly + Assert.Equal("/app/static", resource.ContainerFilesDestination); + } +}