Skip to content
Draft
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
6 changes: 6 additions & 0 deletions CommunityToolkit.Aspire.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
<Folder Name="/examples/flagd/">
<Project Path="examples/flagd/CommunityToolkit.Aspire.Hosting.Flagd.AppHost/CommunityToolkit.Aspire.Hosting.Flagd.AppHost.csproj" />
</Folder>
<Folder Name="/examples/floci/">
<Project Path="examples/floci/CommunityToolkit.Aspire.Hosting.Floci.AppHost/CommunityToolkit.Aspire.Hosting.Floci.AppHost.csproj" />
<Project Path="examples/floci/CommunityToolkit.Aspire.Hosting.Floci.Api/CommunityToolkit.Aspire.Hosting.Floci.Api.csproj" />
</Folder>
<Folder Name="/examples/flyway/">
<Project Path="examples/flyway/01.Basic/01.Basic.csproj" />
<Project Path="examples/flyway/02.ContainerConfiguration/02.ContainerConfiguration.csproj" />
Expand Down Expand Up @@ -244,6 +248,7 @@
<Project Path="src/CommunityToolkit.Aspire.Hosting.DuckDB/CommunityToolkit.Aspire.Hosting.DuckDB.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.Elasticsearch.Extensions/CommunityToolkit.Aspire.Hosting.Elasticsearch.Extensions.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.Flagd/CommunityToolkit.Aspire.Hosting.Flagd.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.Floci/CommunityToolkit.Aspire.Hosting.Floci.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.Flyway/CommunityToolkit.Aspire.Hosting.Flyway.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.GoFeatureFlag/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.csproj" />
<Project Path="src/CommunityToolkit.Aspire.Hosting.Golang/CommunityToolkit.Aspire.Hosting.Golang.csproj" />
Expand Down Expand Up @@ -318,6 +323,7 @@
<Project Path="tests/CommunityToolkit.Aspire.Hosting.DuckDB.Tests/CommunityToolkit.Aspire.Hosting.DuckDB.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Elasticsearch.Extensions.Tests/CommunityToolkit.Aspire.Hosting.Elasticsearch.Extensions.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests/CommunityToolkit.Aspire.Hosting.Flagd.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/CommunityToolkit.Aspire.Hosting.Floci.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests/CommunityToolkit.Aspire.Hosting.Flyway.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests/CommunityToolkit.Aspire.Hosting.GoFeatureFlag.Tests.csproj" />
<Project Path="tests/CommunityToolkit.Aspire.Hosting.Java.Tests/CommunityToolkit.Aspire.Hosting.Java.Tests.csproj" />
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ This repository contains the source code for the Aspire Community Toolkit, a col
| - **Learn More**: [`Hosting.Umami`][umami-integration-docs] <br /> - Stable 📦: [![CommunityToolkit.Aspire.Hosting.Umami][umami-shields]][umami-nuget] <br /> - Preview 📦: [![CommunityToolkit.Aspire.Hosting.Umami][umami-shields-preview]][umami-nuget-preview] | An Aspire hosting integration leveraging the [Umami](https://umami.is/) container. |
| - **Learn More**: [`Hosting.Azure.Extensions`][azure-ext-integration-docs] <br /> - Stable 📦: [![CommunityToolkit.Aspire.Azure.Extensions][azure-ext-shields]][azure-ext-nuget] <br /> - Preview 📦: [![CommunityToolkit.Aspire.Hosting.Azure.Extensions][azure-ext-shields-preview]][azure-ext-nuget-preview] | An integration that contains some additional extensions for hosting Azure container. |
| - **Learn More**: [`Hosting.Squad`][squad-integration-docs] <br /> - Stable 📦: [![CommunityToolkit.Aspire.Hosting.Squad][squad-shields]][squad-nuget] <br /> - Preview 📦: [![CommunityToolkit.Aspire.Hosting.Squad][squad-shields-preview]][squad-nuget-preview] | An Aspire hosting integration that models a [Squad](https://github.com/bradygaster/squad) AI-agent team as a first-class resource. |
| - **Learn More**: [`Hosting.Floci`][floci-integration-docs] <br /> - Stable 📦: [![CommunityToolkit.Aspire.Hosting.Floci][floci-shields]][floci-nuget] <br /> - Preview 📦: [![CommunityToolkit.Aspire.Hosting.Floci][floci-shields-preview]][floci-nuget-preview] | An Aspire hosting integration leveraging the [Floci](https://floci.io) AWS emulator container. |

## 🙌 Getting Started

Expand Down Expand Up @@ -327,3 +328,8 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org)
[squad-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Squad/
[squad-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Squad?label=nuget%20(preview)
[squad-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Squad/absoluteLatest
[floci-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-floci
[floci-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.Floci
[floci-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Floci/
[floci-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Floci?label=nuget%20(preview)
[floci-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Floci/absoluteLatest
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.S3" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.Extensions.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);

// Read AWS config from env vars injected by Aspire's WithReference(floci).
// RegionEndpoint is intentionally omitted — the SDK resolves it from AWS_DEFAULT_REGION
// automatically, and combining ServiceURL + RegionEndpoint in SDK v4 triggers a NPE in
// the endpoint rule engine.
var endpointUrl = Environment.GetEnvironmentVariable("AWS_ENDPOINT_URL") ?? "http://localhost:4566";
var region = Environment.GetEnvironmentVariable("AWS_DEFAULT_REGION") ?? "us-east-1";
var accessKey = Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID") ?? "test";
var secretKey = Environment.GetEnvironmentVariable("AWS_SECRET_ACCESS_KEY") ?? "test";

var s3 = new AmazonS3Client(
new BasicAWSCredentials(accessKey, secretKey),
new AmazonS3Config
{
ServiceURL = endpointUrl,
ForcePathStyle = true
});

builder.Services.AddSingleton<IAmazonS3>(s3);

// Creates the demo bucket once Floci is reachable; logs a warning if Floci is still starting.
builder.Services.AddHostedService<BucketInitializer>();

builder.Services.AddHealthChecks()
.AddAsyncCheck("floci-s3", async ct =>
{
try
{
var response = await s3.ListBucketsAsync(ct);
return HealthCheckResult.Healthy($"Floci S3 reachable — {response.Buckets.Count} bucket(s)");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Floci S3 unreachable", ex);
}
});

var app = builder.Build();

app.Logger.LogInformation(
"Floci endpoint={Url} region={Region} accessKey={Key}",
endpointUrl, region, accessKey);

// /alive — liveness probe (process is up, no dependency checks)
// /health — readiness probe (checks S3 connectivity)
app.MapGet("/alive", () => Results.Ok());
app.MapHealthChecks("/health");

// --- S3 demo endpoints ---

// List all buckets currently in Floci
app.MapGet("/s3/buckets", async (IAmazonS3 s3) =>
{
var response = await s3.ListBucketsAsync();
return response.Buckets.Select(b => new { b.BucketName, b.CreationDate });
});

// Create a new bucket
app.MapPost("/s3/{bucket}", async (string bucket, IAmazonS3 s3) =>
{
await s3.PutBucketAsync(new PutBucketRequest { BucketName = bucket });
return Results.Created($"/s3/{bucket}", new { bucket });
});

// Store a text value at bucket/key
app.MapPut("/s3/{bucket}/{*key}", async (string bucket, string key, HttpRequest request, IAmazonS3 s3) =>
{
using var reader = new StreamReader(request.Body);
var body = await reader.ReadToEndAsync();
await s3.PutObjectAsync(new PutObjectRequest { BucketName = bucket, Key = key, ContentBody = body });
return Results.Created($"/s3/{bucket}/{key}", new { bucket, key, size = body.Length });
});

// Retrieve a value previously stored at bucket/key
app.MapGet("/s3/{bucket}/{*key}", async (string bucket, string key, IAmazonS3 s3) =>
{
try
{
var response = await s3.GetObjectAsync(new GetObjectRequest { BucketName = bucket, Key = key });
using var reader = new StreamReader(response.ResponseStream);
return Results.Ok(await reader.ReadToEndAsync());
}
catch (AmazonS3Exception ex) when (ex.ErrorCode is "NoSuchKey" or "NoSuchBucket")
{
return Results.NotFound(new { bucket, key });
}
});

app.Run();

/// Hosted service that creates the demo bucket once the app starts.
/// Runs concurrently with the health check; failures are non-fatal since
/// Floci may still be initialising when the API first comes up.
class BucketInitializer(IAmazonS3 s3, ILogger<BucketInitializer> logger) : IHostedService
{
private const string DemoBucket = "aspire-demo";

public async Task StartAsync(CancellationToken ct)
{
try
{
var buckets = await s3.ListBucketsAsync(ct);
if (!buckets.Buckets.Any(b => b.BucketName == DemoBucket))
{
await s3.PutBucketAsync(new PutBucketRequest { BucketName = DemoBucket }, ct);
logger.LogInformation("Created demo S3 bucket '{Bucket}'", DemoBucket);
}
else
{
logger.LogInformation("Demo S3 bucket '{Bucket}' already exists", DemoBucket);
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "Demo bucket init deferred — Floci may still be starting");
}
}

public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"CommunityToolkit.Aspire.Hosting.Floci.ApiService": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:56381"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createBuilder } from './.aspire/modules/aspire.mjs';

const builder = await createBuilder();

// ── Runtime path (actually executed) ─────────────────────────────────────────
// Single Floci instance with the Docker socket mounted so Lambda and other
// container-backed AWS services can launch sibling containers.
const floci = await builder.addFloci('floci');

const appHostDirectory = path.dirname(fileURLToPath(import.meta.url));
const apiServiceProject = "CommunityToolkit.Aspire.Hosting.Floci.ApiService";
const apiServiceProjectPath = path.join(appHostDirectory, "..", apiServiceProject, apiServiceProject + ".csproj");

const apiService = await builder.addProject("floci-api", apiServiceProjectPath)
.WithExternalHttpEndpoints()
.WithHttpHealthCheck("/health")
.withReference(floci)
.waitFor(floci);

// ── Compile-time coverage ─────────────────────────────────────────────────────
// Guards with false so these are type-checked but never executed.
// Covers the full exported API surface without requiring Docker in CI.
const includeCompileOnlyScenarios = false;

if (includeCompileOnlyScenarios) {

// ── Custom socket path ────────────────────────────────────────────────────
// Non-standard Docker installations (Podman, Rancher Desktop) expose the
// socket at a different path; pass it explicitly.
const _podman = await builder.addFloci('floci-podman');
await _podman.withDockerSocket('/run/user/1000/podman/podman.sock');

// ── Custom port and region ────────────────────────────────────────────────
const _custom = await builder.addFloci('floci-custom', {
port: 14566,
defaultRegion: 'eu-west-1',
defaultAccountId: '123456789012',
});

// ── Persistent storage — named volume ─────────────────────────────────────
// Switches Floci from in-memory to persistent mode automatically.
const _persistent = await builder.addFloci('floci-persistent');
await _persistent.withDataVolume('floci-data');

// ── Persistent storage — bind mount ───────────────────────────────────────
const _mounted = await builder.addFloci('floci-mount');
await _mounted.withDataBindMount('/tmp/floci-data');

// ── Custom Quarkus config file ─────────────────────────────────────────────
// Mounts application.yml read-only at /deployments/config/application.yml
// inside the container so Quarkus merges it with built-in defaults on startup.
const _configured = await builder.addFloci('floci-configured');
await _configured.withConfigFile('./floci.yml');

// ── TLS — Aspire development certificate ──────────────────────────────────
// Call the standard Aspire API directly on the AddFloci return value.
// The integration automatically sets FLOCI_TLS_ENABLED / FLOCI_TLS_CERT_PATH /
// FLOCI_TLS_KEY_PATH when any certificate is configured.
// Both HTTP and HTTPS are served on the same port (4566).
// ConnectionStringExpression and AWS_ENDPOINT_URL automatically switch to https://.
// Run `aspire certs trust` once to add the dev cert to your system trust store.
//
// const _tls = await builder.addFloci('floci-tls');
// await _tls.withHttpsDeveloperCertificate();

// ── Connection string / endpoint properties ───────────────────────────────
// connectionStringExpression → http://localhost:{port} (host processes)
// http://host.docker.internal:{port} (containers)
const _endpoint = await floci.primaryEndpoint();
const _host = await floci.host();
const _port = await floci.port();
const _connectionString = await floci.connectionStringExpression();

// ── WithReference — AWS env var injection ─────────────────────────────────
// Standard WithReference injects:
// ConnectionStrings__floci = http://localhost:{port}
// AWS_ENDPOINT_URL = http://localhost:{port} (or host.docker.internal for containers)
// AWS_DEFAULT_REGION = us-east-1
// AWS_ACCESS_KEY_ID = test
// AWS_SECRET_ACCESS_KEY = test
const _project = await builder
.addProject('api', '../FlociApi/FlociApi.csproj')
.withReference(floci);

const _container = await builder
.addContainer('worker', 'myorg/worker')
.withReference(floci);
}

await builder.build().run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"appHost": {
"path": "apphost.mts",
"language": "typescript/nodejs"
},
"sdk": {
"version": "13.4.3"
},
"profiles": {
"https": {
"applicationUrl": "https://localhost:29760;http://localhost:28941",
"environmentVariables": {
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:10985",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:13329"
}
}
},
"packages": {
"CommunityToolkit.Aspire.Hosting.Floci": "../../../src/CommunityToolkit.Aspire.Hosting.Floci/CommunityToolkit.Aspire.Hosting.Floci.csproj"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// @ts-check

import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';

export default defineConfig({
files: ['apphost.mts'],
extends: [tseslint.configs.base],
languageOptions: {
parserOptions: {
projectService: true,
},
},
rules: {
'@typescript-eslint/no-floating-promises': ['error', { checkThenables: true }],
},
});
Loading
Loading