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
50 changes: 25 additions & 25 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.3.0",
"commands": [
"csharpier"
],
"rollForward": false
},
"dotnet-inspect": {
"version": "0.26.0",
"commands": [
"dotnet-inspect"
],
"rollForward": false
},
"purview.build": {
"version": "0.7.1",
"commands": [
"purview-build"
],
"rollForward": false
}
}
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.3.0",
"commands": [
"csharpier"
],
"rollForward": false
},
"dotnet-inspect": {
"version": "0.26.0",
"commands": [
"dotnet-inspect"
],
"rollForward": false
},
"purview.build": {
"version": "0.8.0",
"commands": [
"purview-build"
],
"rollForward": false
}
}
}
12 changes: 12 additions & 0 deletions .config/purview-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@
// (assets/images/purview-logo-light.png, linked as Sdk/purview-logo-light.png). A package with
// no rule here, or an entry matching none of its globs, fails validation.
"RequiredContent": {
"purview.containers.environment": [
"lib/$(TFM)/Purview.Containers.Environment.dll",
"lib/$(TFM)/Purview.Containers.Environment.xml",
"README.md",
"purview-logo-light.png"
],
"purview.containers.aspire": [
"lib/$(TFM)/Purview.Containers.Aspire.dll",
"lib/$(TFM)/Purview.Containers.Aspire.xml",
"README.md",
"purview-logo-light.png"
],
// Purview.Containers is the umbrella: it has no buildTransitive of its own, it just depends on
// Core + Wsl + Docker, whose assets flow transitively.
"purview.containers": [
Expand Down
26 changes: 26 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ whenever a target framework, a backend package or the `buildTransitive` assets c
| `src/src/Core` | Backend-neutral abstractions (`Purview.Containers.Core`, `net10.0`, namespace `Purview.Containers`): `IContainer`/`ContainerConfiguration`, `ContainerBuilder`, `ContainerBase`, `IContainerBackend`/`ContainerBackends`, wait strategies, images, networking, mounts, diagnostics |
| `src/src/Wsl` | WSL Containers backend (`Purview.Containers.Wsl`, multi-target `net10.0` facade + `net10.0-windows10.0.19041.0` implementation): `WslContainerBackend`, the shared session runtime, `WslContainer`, `WslContainerSession`, and the portable facade (`WslPayload`, `*.Facade.cs`) that loads the implementation at run time |
| `src/src/Docker` | Docker backend (`Purview.Containers.Docker`, `net10.0`): `DockerContainerBackend` + `DockerContainer` over Testcontainers |
| `src/src/Environment` | Standalone CLI bind-sharing resolver (`Purview.Containers.Environment`, `net10.0`); BCL-only, no backend or Aspire dependency |
| `src/src/Aspire` | Optional `Purview.Containers.Aspire` adapter; translates bind sources at `BeforeResourceStartedEvent` without changing endpoints or named volumes |
| `src/src/<Module>` | Service modules (`Purview.Containers.<Module>`, `net10.0`); each is a thin layer over the abstractions and carries a bespoke `Sdk/README.md` |
| `src/src/<Project>/Sdk` | Package-only assets. `Sdk/README.md` is packed as the package README (suppressing the repo-root README); `Sdk/buildTransitive/**` ships MSBuild assets to consumers (the abstractions' backend registration, each backend package's own registration entry, and the WSL backend's consumer defaults and guards) |
| `src/tests` | TUnit unit (`*.UnitTests`) and container integration (`*.IntegrationTests`) projects: the WSLC suites need a WSLC host, `Docker.IntegrationTests` and `Modules.DockerIntegrationTests` need a Docker daemon |
Expand All @@ -52,6 +54,21 @@ whenever a target framework, a backend package or the `buildTransitive` assets c

## Runtime invariants

- Shared extended APIs cover entrypoint/user/labels, exec overrides, resource copy, one-shot exit waits,
managed bridge networks/aliases, builds and reuse. WSLC must target the SDK-owned session with a
bounded `wslc` subprocess for SDK-missing capabilities; ordinary SDK-only consumers need no CLI.
- Never swallow caller cancellation as a successful exec/stop. Startup is a total five-minute managed
budget; exec defaults to one minute, stop to 15 seconds and cleanup to 30 seconds. Synchronous WSLC
native calls cannot be safely interrupted; never advertise hard zero-hang guarantees.
- Reuse is explicit, named and incompatible with auto-remove. Docker follows Testcontainers;
WSLC reuse is session-scoped. Explicit deletion bypasses reuse retention. Dispose containers before
managed networks or built images. CLI diagnostics must not echo environment/build output.
- Restart an exited WSLC reusable container through the CLI **before** reopening its SDK event handle.
Calling native `Start()` after attaching that handle fails with "Process handle has already been attached".
- Docker pre-start resource archives run inside Testcontainers' synchronous `Created` event, before init
startup, with the startup cancellation token. Do not use its directory mapping overload: it drops empty
directories. Both backends share the archive builder and preserve exact file targets.

- **One shared, process-wide session** (`WslContainerRuntime.Instance`), lazily started, named
`wslc-{pid}-{random8}`. Container isolation comes from unique names and unique host ports, not from extra
sessions.
Expand All @@ -74,6 +91,14 @@ whenever a target framework, a backend package or the `buildTransitive` assets c
- **`DisposeAsync` is idempotent** and never terminates the shared session; a process-exit hook disposes the
session so its name is released.
- **Secrets use `Secret`** and must never reach logs, names, `ToString()` or diagnostics (they are redacted).
- **Docker bind sources on Windows** are translated only for a verified local WSL2 engine: match the
connected loopback TCP daemon's ID against running distributions before invoking that distribution's
`wslpath`. Never assume the default distribution or `/mnt`, rewrite remote/Desktop paths, or change
named volumes and mount access modes.
- The standalone `Environment` package is a separate, opt-in CLI path: it verifies native, running-WSL
and explicit shared-remote candidates with two-way file markers. It does not replace the Docker
backend's daemon-identity detection or the Microsoft WSL Containers backend. Keep the resolver
BCL-only and the Aspire dependency in the adapter; neither is included in the umbrella package.

## Consumer requirements and compatibility

Expand Down Expand Up @@ -243,6 +268,7 @@ npx skills add purview-dev/containers
| `skills/purview-containers-lifecycle/` | start/stop/dispose semantics, cleanup guarantees, wait strategies, WSLC networking |
| `skills/purview-containers-testing/` | the test tiers, why modules run serially, pinning a backend, the CI shape |
| `skills/purview-containers-consumer-requirements/` | the target-framework contract, `PCC0001`/`PCC0002` and the other consumer build errors |
| `skills/purview-containers-capabilities/` | parity APIs, networks/aliases, copy, builds, reuse, cancellation and SDK/CLI boundaries |

Every skill carries the `purview-containers` prefix. Skills install into one flat directory per
agent, shared with every skill a developer has installed from anywhere, so an unprefixed name
Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="9.0.21" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Aspire.Hosting" Version="13.6.4" />
<PackageVersion Include="Microsoft.WSL.Containers" Version="$(MicrosoftWSLContainersVersion)" />
<PackageVersion Include="Testcontainers" Version="4.16.0" />
<PackageVersion Include="Microsoft.Extensions.Telemetry.Abstractions" Version="10.11.0" />
Expand All @@ -52,7 +53,7 @@
OSI-approved open-source projects; it never ships in a Purview package.
-->
<PackageVersion Include="Microsoft.Testing.Extensions.Retry" Version="2.5.1" />
<PackageVersion Include="Purview.Skills.Testing" Version="1.1.0" />
<PackageVersion Include="Purview.Skills.Testing" Version="1.2.0" />
<PackageVersion Include="Bogus" Version="35.6.5" />
<PackageVersion Include="Azure.Core" Version="1.63.0" />
<PackageVersion Include="Azure.Security.KeyVault.Certificates" Version="4.9.2" />
Expand Down
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ A Testcontainers-style library for .NET that runs throwaway Linux containers for
> **[Backends: WSLC or Docker](docs/wiki/Backends.md)** — comparison, side-by-side project setup, CI
> example and troubleshooting.

The WSLC backend is built directly against the `Microsoft.WSL.Containers` NuGet package (the WSLC managed C# API) — no `wslc.exe`/`wsl.exe`/`docker` CLI. The Docker backend is built on [Testcontainers for .NET](https://dotnet.testcontainers.org/).
The WSLC backend uses `Microsoft.WSL.Containers` for ordinary lifecycle operations and a bounded
`wslc` CLI fallback for entrypoint/user/labels, managed networks, reuse, resource copy and image builds.
It requires no Docker installation. The Docker backend is built on
[Testcontainers for .NET](https://dotnet.testcontainers.org/).

Both backends expose file copy/read, one-shot exit waits, managed bridge networks/aliases and
Dockerfile builds through the same contract. Startup has a five-minute total budget; exec defaults
to one minute, stop to 15 seconds and cleanup to 30 seconds, all configurable. Cancellation is
propagated; synchronous native WSLC calls cannot be forcibly interrupted. See
[Capabilities and Cancellation](docs/wiki/Backend-Capabilities.md) for APIs, ownership and limits.

> **Experimental.** This project is an experiment in running throwaway containers through Microsoft
> WSL Containers. The public API, defaults and packaging rules can change between prereleases, and
Expand All @@ -36,6 +45,20 @@ The WSLC backend is built directly against the `Microsoft.WSL.Containers` NuGet

## Prerequisites

### Standalone bind sharing and Aspire

`Purview.Containers.Environment` resolves bind paths for Docker-compatible CLI consumers without
referencing the umbrella package, Testcontainers or WSLC. It verifies native paths, running WSL
translations and explicit shared-remote mappings against the daemon using two-way file markers.
`Purview.Containers.Aspire` adds opt-in `builder.AddContainerEnvironment()` integration for any
Aspire AppHost. Both target `net10.0`; the adapter requires Aspire 13.6.4 or later.
These are separate from the existing Docker backend's automatic WSL2 path handling.
Docker Engine inside a WSL distribution is not Microsoft WSL Containers.
See the [environment package guide](src/src/Environment/Sdk/README.md) and
[Aspire adapter guide](src/src/Aspire/Sdk/README.md) for setup, permissions and remote-host limits.

### Container backends

Pick a backend — see [Backends: WSLC or Docker](docs/wiki/Backends.md) for the comparison — or skip the
choice: reference the umbrella `Purview.Containers` and it brings the abstractions plus both backends,
selecting WSLC on Windows and Docker elsewhere automatically. Pick a single backend below only when you
Expand All @@ -54,6 +77,8 @@ want to fix the runtime.
**Docker backend**

- Any reachable Docker daemon, and a `net10.0` (or later) project on any platform.
- Windows bind mounts are translated automatically for a verified local Docker Engine inside WSL2;
no consumer path setting is needed. See [WSL2 bind mounts](docs/wiki/Backends.md#windows-bind-mounts-with-docker-engine-in-wsl2).

.NET SDK 11 is required to build this repository (it pins `11.0.100-rc.1`); SDK 10 is enough for a Docker
consumer.
Expand Down Expand Up @@ -145,6 +170,8 @@ src/
Core/ backend-neutral abstractions (Purview.Containers.Core)
Wsl/ WSL Containers backend (Purview.Containers.Wsl)
Docker/ Docker backend (Purview.Containers.Docker)
Environment/ standalone verified CLI bind sharing (no backend dependencies)
Aspire/ optional Aspire adapter over Environment
Containers/ umbrella package (Purview.Containers): Core + both backends
PostgreSql/ Redis/ MsSql/ MySql/
RabbitMq/ Azurite/ Nats/ AzureKeyVaultEmulator/
Expand Down Expand Up @@ -301,7 +328,7 @@ just sample-docker # needs a Docker daemon
### Verifying the consumer contract

```powershell
just verify-consumers # pack, then build 21 throwaway consumer projects against ./artifacts
just verify-consumers # pack, then build 23 throwaway consumer projects against ./artifacts
just verify-consumers -Keep # same, keeping the generated projects for inspection
```

Expand Down
13 changes: 11 additions & 2 deletions docs/wiki/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,20 @@ Purview.Containers.Wsl (net10.0 facade + net10.0-windows10.0.1
│ ├─ PortAllocator -> native random host ports (windowsPort=0)
│ └─ SessionHandle -> Microsoft.WSL.Containers.Session (internal)
├─ WslContainer : IContainer WSLC-backed container
└─ WslContainerSession : IContainerSession image pull + container create/start/stop/delete/exec
├─ WslContainerSession : IContainerSession image pull + container create/start/stop/delete/exec
└─ WslCommand / WslResource / WslFileCopy bounded CLI fallback in that same session

IContainer : IConnectionStringProvider, IAsyncDisposable
StartAsync / StopAsync / DisposeAsync / ExecAsync / GetMappedPublicPort /
GetConnectionString / GetLogsAsync / tailing IAsyncEnumerable
CopyAsync / ReadFileAsync / WaitForExitAsync / DeleteAsync
```

`IContainerBackend.CreateNetworkAsync` and `BuildImageAsync` return owned `IContainerResource`
instances on either backend. Docker delegates to Testcontainers. WSLC CLI-backed resources target
the SDK-owned session/store, while ordinary lifecycle operations remain SDK-backed. Dispose containers
before networks/images. See [Capabilities and Cancellation](Backend-Capabilities.md).

A module (`Purview.Containers.PostgreSql`, `Purview.Containers.Redis`, …) derives its container from
`ContainerBase` and references `Purview.Containers.Core` only. The backend is resolved at run time through
`ContainerBackends`, so the same module package works on WSLC or Docker. `Purview.Containers` is the
Expand Down Expand Up @@ -141,7 +148,9 @@ Isolated stores are transient and are removed when their session terminates.
## Observability

- `System.Diagnostics.ActivitySource("Purview.Containers")` emits `wslcontainer.session.start`, `wslcontainer.image.pull`, `wslcontainer.container.create/start/stop/delete`, `wslcontainer.exec.create`, and `wslcontainer.wait`. Tags carry container name/id/image and strategy — never credentials.
- Tailing `GetLogsAsync(CancellationToken)` ends when the container's init process exits: the log buffer is flushed and its channel completed on process exit (and on container disposal), so an `await foreach` over the stream terminates instead of waiting forever. The string overload (`GetLogsAsync(stream, ct)`) reads the accumulated buffer only.
- Tailing `GetLogsAsync(CancellationToken)` polls output until live container state reports exit; it does
not depend solely on native exit events. The snapshot overload uses the SDK buffer or CLI logs for
CLI-created containers. Always supply an enumeration token.
- `Microsoft.Extensions.Logging` integration is optional/future; the core works without a host or DI.

## Extensibility
Expand Down
Loading
Loading