Skip to content

Commit 3af555c

Browse files
adds instruction to hosting integration custom agent (#980)
1 parent 3c23498 commit 3af555c

File tree

1 file changed

+283
-0
lines changed

1 file changed

+283
-0
lines changed

.github/agents/hosting-integration-creator.agent.md

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,286 @@ Key points:
339339
### Auto-Generated `api` Folder Warning
340340

341341
Do NOT create or manually edit an `api` folder or any files within it for hosting integrations. Files under paths like `src/CommunityToolkit.Aspire.Hosting.[HostingName]/api/` are generated automatically (e.g. by source generators or build tooling). Manual changes will be overwritten and should instead be implemented in normal source files outside `api`. If you need new generated capabilities, extend the generator or add new partial types outside the `api` directory.
342+
343+
## Language-Based Hosting Integrations
344+
345+
When creating hosting integrations for programming languages (e.g., Golang, Rust, Python, Node.js, Java), there are additional capabilities and patterns to consider beyond basic executable hosting:
346+
347+
### Package Manager Support
348+
349+
Language-based hosting integrations should support the ecosystem's package managers and dependency management:
350+
351+
- **Build Tags/Features**: Allow users to specify conditional compilation flags (e.g., Go build tags: `buildTags: ["dev", "production"]`)
352+
- **Dependency Installation**: Optionally support automatic dependency installation before running (e.g., `go mod download`, `npm install`)
353+
- **Executable Path Flexibility**: Support both default entry points (e.g., `"."` for Go) and custom paths (e.g., `"./cmd/server"`)
354+
355+
Example from Golang integration:
356+
357+
```csharp
358+
public static IResourceBuilder<GolangAppExecutableResource> AddGolangApp(
359+
this IDistributedApplicationBuilder builder,
360+
[ResourceName] string name,
361+
string workingDirectory,
362+
string executable,
363+
string[]? args = null,
364+
string[]? buildTags = null)
365+
{
366+
var allArgs = new List<string> { "run" };
367+
368+
if (buildTags is { Length: > 0 })
369+
{
370+
allArgs.Add("-tags");
371+
allArgs.Add(string.Join(",", buildTags));
372+
}
373+
374+
allArgs.Add(executable);
375+
// ... rest of implementation
376+
}
377+
```
378+
379+
### Publish as Container (Multi-Stage Builds)
380+
381+
Language-based integrations should automatically generate optimized Dockerfiles using multi-stage builds when publishing:
382+
383+
- **Build Stage**: Uses the language's official SDK image to compile/build the application
384+
- **Runtime Stage**: Uses a minimal base image (e.g., Alpine Linux) for smaller final images
385+
- **Security**: Install necessary certificates (e.g., CA certificates for HTTPS support)
386+
- **Optimization**: Disable unnecessary features in build (e.g., `CGO_ENABLED=0` for Go)
387+
388+
Example from Golang integration:
389+
390+
```csharp
391+
private static IResourceBuilder<GolangAppExecutableResource> PublishAsGolangDockerfile(
392+
this IResourceBuilder<GolangAppExecutableResource> builder,
393+
string workingDirectory,
394+
string executable,
395+
string[]? buildTags)
396+
{
397+
const string DefaultAlpineVersion = "3.21";
398+
399+
return builder.PublishAsDockerFile(publish =>
400+
{
401+
publish.WithDockerfileBuilder(workingDirectory, context =>
402+
{
403+
var buildArgs = new List<string> { "build", "-o", "server" };
404+
405+
if (buildTags is { Length: > 0 })
406+
{
407+
buildArgs.Add("-tags");
408+
buildArgs.Add(string.Join(",", buildTags));
409+
}
410+
411+
buildArgs.Add(executable);
412+
413+
// Get custom base image from annotation, if present
414+
context.Resource.TryGetLastAnnotation<DockerfileBaseImageAnnotation>(out var baseImageAnnotation);
415+
var goVersion = baseImageAnnotation?.BuildImage ?? GetDefaultGoBaseImage(workingDirectory, context.Services);
416+
417+
var buildStage = context.Builder
418+
.From(goVersion, "builder")
419+
.WorkDir("/build")
420+
.Copy(".", "./")
421+
.Run(string.Join(" ", ["CGO_ENABLED=0", "go", .. buildArgs]));
422+
423+
var runtimeImage = baseImageAnnotation?.RuntimeImage ?? $"alpine:{DefaultAlpineVersion}";
424+
425+
context.Builder
426+
.From(runtimeImage)
427+
.Run("apk --no-cache add ca-certificates")
428+
.WorkDir("/app")
429+
.CopyFrom(buildStage.StageName!, "/build/server", "/app/server")
430+
.Entrypoint(["/app/server"]);
431+
});
432+
});
433+
}
434+
```
435+
436+
### TLS/HTTPS Support
437+
438+
For language integrations that may need secure connections:
439+
440+
- **CA Certificates**: Install CA certificates in runtime image for HTTPS client requests
441+
- **Runtime Configuration**: Ensure the generated container supports TLS connections (e.g., `apk --no-cache add ca-certificates`)
442+
443+
Example from Golang Dockerfile generation:
444+
445+
```csharp
446+
context.Builder
447+
.From(runtimeImage)
448+
.Run("apk --no-cache add ca-certificates") // Enables HTTPS support
449+
.WorkDir("/app")
450+
// ... rest of configuration
451+
```
452+
453+
### Version Detection
454+
455+
Automatically detect and use the appropriate language version from project files:
456+
457+
- **Project Files**: Parse version from language-specific files (e.g., `go.mod`, `package.json`, `Cargo.toml`)
458+
- **Installed Toolchain**: Fall back to the installed language toolchain version
459+
- **Default Version**: Use a sensible default if detection fails
460+
461+
Example from Golang integration:
462+
463+
```csharp
464+
internal static string? DetectGoVersion(string workingDirectory, ILogger logger)
465+
{
466+
// Check go.mod file
467+
var goModPath = Path.Combine(workingDirectory, "go.mod");
468+
if (File.Exists(goModPath))
469+
{
470+
try
471+
{
472+
var goModContent = File.ReadAllText(goModPath);
473+
// Look for "go X.Y" or "go X.Y.Z" line in go.mod
474+
var match = Regex.Match(goModContent, @"^\s*go\s+(\d+\.\d+(?:\.\d+)?)", RegexOptions.Multiline);
475+
if (match.Success)
476+
{
477+
var version = match.Groups[1].Value;
478+
// Extract major.minor (e.g., "1.22" from "1.22.3")
479+
var versionParts = version.Split('.');
480+
if (versionParts.Length >= 2)
481+
{
482+
var majorMinor = $"{versionParts[0]}.{versionParts[1]}";
483+
logger.LogDebug("Detected Go version {Version} from go.mod file", majorMinor);
484+
return majorMinor;
485+
}
486+
}
487+
}
488+
catch (IOException ex)
489+
{
490+
logger.LogDebug(ex, "Failed to parse go.mod file due to IO error");
491+
}
492+
}
493+
494+
// Try to detect from installed Go toolchain
495+
try
496+
{
497+
var startInfo = new ProcessStartInfo
498+
{
499+
FileName = "go",
500+
Arguments = "version",
501+
RedirectStandardOutput = true,
502+
RedirectStandardError = true,
503+
UseShellExecute = false,
504+
CreateNoWindow = true
505+
};
506+
507+
using var process = Process.Start(startInfo);
508+
if (process != null)
509+
{
510+
var output = process.StandardOutput.ReadToEnd();
511+
process.WaitForExit();
512+
513+
if (process.ExitCode == 0)
514+
{
515+
var match = Regex.Match(output, @"go version go(\d+\.\d+)");
516+
if (match.Success)
517+
{
518+
var version = match.Groups[1].Value;
519+
logger.LogDebug("Detected Go version {Version} from installed toolchain", version);
520+
return version;
521+
}
522+
}
523+
}
524+
}
525+
catch (Exception ex)
526+
{
527+
logger.LogDebug(ex, "Failed to detect Go version from installed toolchain");
528+
}
529+
530+
logger.LogDebug("No Go version detected, will use default version");
531+
return null;
532+
}
533+
```
534+
535+
### Optimizing and Securing Container Images
536+
537+
Allow users to customize base images for both build and runtime stages:
538+
539+
- **Customizable Build Image**: Let users override the builder/SDK image (e.g., `golang:1.22-alpine` instead of `golang:1.22`)
540+
- **Customizable Runtime Image**: Let users override the runtime image (e.g., `alpine:3.20` instead of `alpine:3.21`)
541+
- **Annotation-Based Configuration**: Use annotations to store custom base image settings
542+
543+
Example annotation and extension method:
544+
545+
```csharp
546+
// Annotation to store custom base images
547+
internal sealed record DockerfileBaseImageAnnotation : IResourceAnnotation
548+
{
549+
public string? BuildImage { get; init; }
550+
public string? RuntimeImage { get; init; }
551+
}
552+
553+
// Extension method to configure base images
554+
public static IResourceBuilder<TResource> WithDockerfileBaseImage<TResource>(
555+
this IResourceBuilder<TResource> builder,
556+
string? buildImage = null,
557+
string? runtimeImage = null)
558+
where TResource : IResource
559+
{
560+
return builder.WithAnnotation(new DockerfileBaseImageAnnotation
561+
{
562+
BuildImage = buildImage,
563+
RuntimeImage = runtimeImage
564+
});
565+
}
566+
```
567+
568+
Usage example:
569+
570+
```csharp
571+
var golang = builder.AddGolangApp("golang", "../gin-api")
572+
.WithHttpEndpoint(env: "PORT")
573+
.WithDockerfileBaseImage(
574+
buildImage: "golang:1.22-alpine",
575+
runtimeImage: "alpine:3.20");
576+
```
577+
578+
### Documentation Requirements
579+
580+
For language-based integrations, the README.md should include:
581+
582+
- **Publishing Section**: Explain automatic Dockerfile generation
583+
- **Version Detection**: Document how version detection works
584+
- **Customization Options**: Show how to customize base images
585+
- **TLS Support**: Note that CA certificates are included for HTTPS
586+
- **Build Options**: Document package manager flags, build tags, etc.
587+
588+
Example README structure:
589+
590+
```markdown
591+
## Publishing
592+
593+
When publishing your Aspire application, the [Language] resource automatically generates a multi-stage Dockerfile for containerization.
594+
595+
### Automatic Version Detection
596+
597+
The integration automatically detects the [Language] version to use by:
598+
1. Checking the [project file] for the version directive
599+
2. Falling back to the installed toolchain version
600+
3. Using [version] as the default if no version is detected
601+
602+
### Customizing Base Images
603+
604+
You can customize the base images used in the Dockerfile:
605+
606+
[code example]
607+
608+
### Generated Dockerfile
609+
610+
The automatically generated Dockerfile:
611+
- Uses the detected or default [Language] version as the build stage
612+
- Uses a minimal runtime image for a smaller final image
613+
- Installs CA certificates for HTTPS support
614+
- Respects build options if specified
615+
```
616+
617+
### Testing Considerations
618+
619+
When testing language-based integrations:
620+
621+
- **Unit Tests**: Verify build arguments, version detection logic, and annotation handling
622+
- **Integration Tests**: Test the full publishing workflow if possible
623+
- **Version Detection Tests**: Mock file system and process execution to test version detection
624+
- **Dockerfile Generation**: Verify the generated Dockerfile structure matches expectations

0 commit comments

Comments
 (0)