You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
DocumentDBVersion.V0_116_0 curated enum member and DocumentDBVersions.V0_116_0 = "0.116.0" constant. Upstream skipped the v0.115-0 release and rolled its prepared changes into v0.116-0, so there is intentionally no V0_115_0 member.
Changed
DocumentDBVersions.Latest and the default documentdb-local image now resolve to 0.116.0 and ghcr.io/documentdb/documentdb/documentdb-local:pg17-0.116.0. Upstream user-visible changes include $jsonSchema support for enum and oneOf, wire-compatible $sample size validation, indexed streaming and planning improvements for grouped and composite queries, and reuse of warm gateway PostgreSQL connection pools.
Published manifests now route the connection-string credentials through annotated.string companion resources (<parameter>-uri-encoded, with "filter": "uri") instead of referencing the credential parameters directly, because a manifest cannot inline the encoded value. The downstream publisher implements that filter, so the escaping applied at deployment time is the publisher's rather than Aspire's. Notably, azd's current Container Apps generation implements uri with Go's url.QueryEscape, which encodes a literal space as +; MongoDB and libpq decode userinfo per RFC 3986 and do not convert + back into a space, so credentials containing spaces are not guaranteed to round-trip through an azd deployment even though they resolve correctly under aspire run. Delimiters and non-ASCII text are escaped compatibly by both. See configuration.md.
Adopted the DocumentDB 0.116.0 Local image runtime contract across PG15-PG18, including its /data image volume, single-container data-directory lock, one-shot initialization state, reserved username prefixes, and lz4 TOAST default. Persistence, initialization-readiness, external PostgreSQL, and OTLP metrics behavior are documented and covered by Docker-backed tests, including a PG17 0.114.0 to 0.116.0 data upgrade.
Documented the DocumentDB storage contract in a new "Storage requirements" section of the configuration reference, with matching troubleshooting entries: the data directory must be writable, must be empty or hold an existing cluster, is owned by the container's documentdb user, and has version-specific volume, locking, and initialization behavior. From 0.116.0, /data is an image volume, the directory is protected by an exclusive lock, and marker-backed initialization is one-shot. At or below 0.114.0, requested initialization runs on every container start, so seed scripts used with persisted storage must be idempotent.
Container-backed bind-mount persistence coverage now requires the restarted container to serve the persisted data directory on every runtime except Docker Desktop, which is identified from docker info's OperatingSystem field rather than from the host OS (falling back to "not Docker Desktop on a Linux host" if the daemon cannot be asked). A refusal anywhere else fails with the container's own log and the runtime identification. The refusal is recognised only when the current run failed to start PostgreSQL and logged the ownership error at or after the container's start time, so the previous run's failure — replayed into the next container out of the persisted pglog.log the entrypoint tails — cannot be mistaken for a refusal by a run that actually recovered.
Fixed
The WithPostgresEndpoint() credential floor and the Pg18 publish floor are now judged on the image the resource will actually run. Both were ordinary BeforeResourceStartedEvent subscribers, and a subscriber registered after AddDocumentDB runs after them: nothing is published between that point and the container's creation, and the argument gather that follows records the already-changed image as the initial state, so a .WithImageTag("pg17-0.111.0") or .WithImageTag("pg18-0.113.0") applied there started a container that cannot authenticate with the Aspire-generated postgresql:// URI, or one whose tag has never existed on GHCR. The two floors are now stated once and re-applied from the package's existing uncached authority — the container-runtime-arguments callback immediately before each container creation, and the manifest publishing callback while the resource is actually serialized — so a publish is refused with the same actionable message rather than emitting a manifest naming an unusable image. In a run the image those checkpoints judge is the sealed one rather than the annotation as it then reads, and an image changed after the seal is refused as a change instead of re-judged; see the image-seal entry below. ExcludeFromManifest() remains the boundary, and the documented carve-outs are unchanged: custom repositories, tags outside the pg{NN}-X.Y.Z grammar, digest pins and Dockerfile builds carry no version to judge, early or late.
The data-storage rules now record what they judged and re-check it where Aspire caches nothing, so a mount added or replaced after the verdict can no longer slip past them. Aspire records each callback's result the first time it runs and reuses it for the rest of the run, and a volume or bind mount is a plain annotation, so anything that built the resource's configuration early — the public ExecutionConfigurationBuilder from an IDistributedApplicationLifecycleHook or an event subscriber — could then mount /data read-only, or put a second resource on the same named volume or host directory, without a single rule running again: the container would start on storage DocumentDB 0.116.0 cannot take ownership of, and a manifest could be published with two clusters on one directory. The mounts (by value, so re-declaring the same storage or reordering it is not a change), the membership of the environment and command-line pipelines, the explicit-start setting and the effective image are now recorded beside the command-line seal and compared by the same authority, from the same two uncached checkpoints. The publish checkpoint moved from BeforePublishEvent to the manifest publishing callback, which runs while the resource is being serialized — later than any model event, so a subscriber registered after AddDocumentDB can no longer run after it; the event is still used to take the last position back. Nothing is repaired or re-judged: the resource is failed with an InvalidOperationException that names what kind of thing changed and never repeats a value. A resource excluded from the manifest is still excluded, a caller's own manifest writer still writes it, and the published manifest is otherwise unchanged.
The WithOpenTelemetryMetrics(...) gateway wrapper and the data-storage command-line rule are no longer bypassed by reading the resource's configuration before changing it. Aspire records each callback's result the first time it runs and reuses it for the rest of the run, and the argument gatherer takes the last callback's recorded result as the whole argument list. A caller who built the configuration through the public ExecutionConfigurationBuilder (or the obsolete GetArgumentValuesAsync) — from an IDistributedApplicationLifecycleHook, or as the dependency pass a container creation performs before it builds its spec — and only then appended an argument callback, re-pointed Entrypoint, or selected another image got a validated answer recorded before the change and reused after it. Appending a callback in that state did not reorder the command line, it replaced it: the recorded wrapper and the recorded reserved---data-path scan were both dropped, and the manifest carried only the late callback's arguments. Run mode had the same hole through WithContainerRuntimeArgs(...), which Aspire never caches and invokes after the dependency pass but before the container's command is read. The one command-line callback this package owns now records what its answer depended on — the callbacks in the command-line, environment and container-runtime pipelines, the entrypoint, and the effective image — and that record is compared at the two phases Aspire runs unconditionally: a container-runtime-arguments callback the package owns and keeps last, which runs on every container creation after any caller callback and before the command, arguments and environment are read; and BeforePublishEvent, which is raised after every lifecycle hook and before the publishing pipeline serializes anything. A mismatch fails the resource before the container is created or the manifest is written, naming what kind of thing changed and no value, since whatever changed the model may be carrying a secret. An environment callback that re-points the entrypoint is covered too: Aspire's dependency pass evaluates environment callbacks before argument callbacks, so the wrapper's own callback sees the result and refuses. Arguments added by a lifecycle hook that does not read first are now ordered behind the wrapper — and in front of the storage rule, which rejects a reserved --data-path on its own terms — where they previously failed the resource with an ordering error: BeforePublishEvent is late enough to repair them. The data-storage environment guard is unchanged, annotations are still moved rather than re-created, so every callback is evaluated exactly once per run, re-evaluated on restart, and a deferred or secret-bearing argument is resolved by Aspire once and never by this package.
The WithOpenTelemetryMetrics(...) gateway configuration wrapper is now the last word on the container command line, so a later argument callback can no longer displace it, and it shares that position with the data-storage guard's command-line rule instead of competing with it. Aspire evaluates command-line callbacks in annotation order over one shared list, and the wrapper's callback used to be registered where WithOpenTelemetryMetrics(...) was called; a WithArgs(...) registered after it therefore ran last. /bin/bash reads its command from the first arguments, so .WithOpenTelemetryMetrics().WithArgs(context => context.Args.Insert(0, "--help")) published and ran /bin/bash --help -c <script> --, and bash exited without starting DocumentDB. The package now owns exactly one command-line callback per resource, running the storage reserved---data-path rule first — on the caller's own arguments, as the image entrypoint's --option value grammar reads them — then the wrapper, then the checks. It retakes the last position at BeforeStartEvent, ResourceEndpointsAllocatedEvent and BeforeResourceStartedEvent, which covers every builder-time WithArgs(...) whatever the call order, a BeforeStartEvent subscriber registered after AddDocumentDB, and an IDistributedApplicationLifecycleHook. Caller arguments that prepend, clear, reorder or replace the list now run before the wrapper, so what they produce becomes arguments of the image entrypoint, which is what they were asking for; appending is unchanged. Anything that appends a command-line callback after the last phase — a BeforeResourceStartedEvent subscriber registered after AddDocumentDB, or, in publish mode, any lifecycle hook, because publish raises no per-resource event — still fails the resource, now with one message for both rules, and without repeating an argument value that may be a secret. The finished command line is verified before it is used: the reserved---data-path rule is re-checked past the wrapper prefix, the entrypoint must still be /bin/bash, the arguments must begin with exactly -c, this run's script instance and --, the script must not appear twice, and the image is classified again so a late image, tag, digest or Dockerfile change is judged on what the container will actually run. The data-storage guard's environment pipeline is unchanged, annotations are moved rather than re-created so every callback is still evaluated exactly once per run and re-evaluated on restart, and the published manifest still carries the same brace-free one-line script in entrypoint/args.
The official documentdb-local image is now recognised from the fully composed image reference rather than from ContainerImageAnnotation.Image alone, and the registry field is no longer trusted to hold a registry. Aspire joins the two annotation fields with a separator and validates neither, so WithImage("ghcr.io/documentdb/documentdb/documentdb-local", "pg17-0.116.0").WithImageRegistry(null) publishes and runs exactly the reference the default spelling does; comparing the image field on its own called that a custom image and withheld everything that is true of the official one: the /data volume declaration and the data-directory flock, the WithPostgresEndpoint() credential floor, the Pg18 publish floor, and the WithOpenTelemetryMetrics(...) gateway configuration wrapper and its digest rejection. Recognition now composes the reference first and then removes exactly one prefix — either the curated registry ghcr.io/documentdb, or a single registry host (DNS name, IPv4 or bracketed IPv6 literal, or localhost, each with an optional port) with nothing after it — and what remains must be documentdb/documentdb-local exactly. Two spellings that compose the same reference therefore always classify the same. A private mirror is the curated repository directly beneath a registry host, in either spelling, such as contoso.azurecr.io/documentdb/documentdb-local or localhost:5000/documentdb/documentdb-local. A namespace, project or mirror path in front of the repository names a different repository and stays custom, whether it is written into the registry field or inline: .WithImageRegistry("ghcr.io/evil"), .WithImageRegistry("contoso.azurecr.io/mirrors") and .WithImageRegistry("harbor.corp.local/library") over documentdb/documentdb-local are now custom where a registry-prefixed spelling was previously accepted verbatim, as are evil/documentdb/documentdb-local, a bare registry name with no port such as myregistry/documentdb/documentdb-local, a reference that repeats the registry in the image field (composing an unresolvable ghcr.io/documentdb/ghcr.io/...), and any reference with a doubled, leading or trailing separator. A digest is read whether it arrives through WithImageSHA256(...) or inline as repository@sha256:..., and a : is a tag only in the last path segment, so a registry port is never mistaken for one. Dockerfile builds remain custom whatever their image text says. A digest now beats every tag: a reference carrying both — repository:pg17-0.116.0@sha256:..., an inline tag beside a WithImageSHA256(...) digest, or the reverse — is resolved by the runtime from the digest, so it is treated as version unknown and no longer inherits the tag's release. Previously such a reference could take 0.116.0 assumptions from the tag while running an older image, which downgraded a shared-data-directory failure to a warning on an interlock that image may not have, raised the declared-/data-volume warning, and enforced or refused the version floors against a tag the runtime discards. The repository is still recognised, so WithOpenTelemetryMetrics(...) still rejects a digest-pinned curated image with its actionable message, and a digest on any other repository remains untouched.
The WithOpenTelemetryMetrics(...) gateway wrapper now keeps its sanitized SetupConfiguration.json outside the effective canonical DATA_PATH, including a -d or --data-path command-line override. It previously used the default mktemp -d location, so a valid data path of /tmp caused the wrapper to create a directory inside the fresh PostgreSQL data target before initialization; DocumentDB 0.116.0 then correctly refused the non-empty non-cluster directory. The wrapper now selects a writable subtree from /tmp, /var/tmp, and /dev/shm that is disjoint from DATA_PATH both as a container path and as storage, and fails clearly if none is safe. The storage half matters because two container paths that do not contain one another can still be one directory: one host directory bind-mounted at both /tmp and /data, or one named volume mounted twice, put the scratch copy straight back inside the data directory. The mount table is known while the model is built, so the aliasing decision is made there — including mount targets that are ancestors of the data directory or of a candidate root, and the relative subpaths below them — and travels with the container command as the exact set of data directories each candidate cannot be used with, which keeps the runtime -d/--data-path override working. Both enabled and explicitly disabled metrics are covered because both modes install the wrapper.
A DocumentDB resource whose container image is built from a Dockerfile (WithDockerfile(...), WithDockerfileFactory(...), WithDockerfileBuilder(...)) is now classified as a custom image of unknown version for every version-dependent behaviour, whatever its image annotation says. Aspire keeps the ContainerImageAnnotation that AddDocumentDB installs when a Dockerfile build is added, and a caller can re-point it at documentdb/documentdb-local and a recognised pg{NN}-X.Y.Z tag afterwards, so such a resource could previously be mistaken for an official 0.116.0 image and granted behaviour that had not been established for it: the shared-data-directory hard failure was downgraded to a warning by WithExplicitStart() on the strength of a flock the build may not perform, the declared-/data-volume warning was raised for a VOLUME the build may not declare, the WithPostgresEndpoint() credential floor and the Pg18 publish floor were enforced against a tag the runtime never resolves, and WithOpenTelemetryMetrics(...) replaced the container entrypoint with a wrapper built entirely out of official-image facts — the emulator_entrypoint.sh path, the packaged /etc/documentdb/gateway layout, bash and jq — or threw on a digest pin that is no more what runs than the tag is. The published manifest emits a build object and no image at all, which is what makes the annotation unusable as evidence. Image classification is now resolved in one place for all of these; true official images, private mirrors, digest pins, recognised tags, unrecognised tags and custom repositories are unaffected, and overriding only the base image of a generated Dockerfile (WithDockerfileBaseImage(...)) is correctly not treated as a build.
Read-only DocumentDB data storage now fails immediately instead of producing a misleading startup failure. WithDataVolume(isReadOnly: true) and WithDataBindMount(..., isReadOnly: true) throw an ArgumentException, and a read-only volume or bind mount placed on the data path with the raw Aspire WithVolume/WithBindMount APIs fails the resource start with an InvalidOperationException. The rejection names the target path actually requested. WithInitData(...) and WithTlsCertificate(...) still mount their inputs read-only, which is correct and unaffected.
Sharing one data directory between two DocumentDB resources in the same application model now fails at start with an explanatory InvalidOperationException naming both resources. From 0.116.0 the container defends itself with an exclusive flock; at or below 0.114.0 there is no interlock. The combination is downgraded to a warning only when both resources resolve to a recognized 0.116.0-or-later tag and one is started manually with WithExplicitStart().
Two mounts on the same DocumentDB data path now fail at start with an actionable message rather than letting the container runtime reject the duplicate mount target.
Corrected the documented handling of a data directory that is not empty and not a PostgreSQL cluster. The container refuses it, never starts PostgreSQL, and leaves the contents intact; a stray .gitkeep or .DS_Store is enough to trigger the failure.
WithDataVolume(targetPath: ...) now validates and canonicalizes the target path. Empty, whitespace, and relative paths are rejected with an ArgumentException, and repeated separators, . and .. segments are resolved as the container runtime resolves them, so /data/, //data, and /foo/../data all land on the image's declared /data volume. A target that resolves to the container root (/data/..) is rejected because the runtime refuses destination can't be '/'; one that reaches above the root (/../data) is rejected for the opposite reason, that the runtime accepts it — it clamps the destination and mounts on /data, so the storage lands on a path the call never named.
The data-directory guards now run inside the resource's own configuration pipeline instead of beside it. They are appended as the last environment and command-line callbacks when the application starts, so they observe the final DATA_PATH and the final argument list — including values produced by dynamic callbacks, in whatever order the calls were made — and the canonical DATA_PATH they validated replaces the value the container is given. Aspire evaluates each callback once per run, so a callback that computes a different path every time it is asked cannot make the guard and the container disagree, and nothing but DATA_PATH is resolved: the password and every other environment value are left as the callbacks produced them. Previously the guards ran their own copy of the environment pipeline from a BeforeResourceStartedEvent handler, which evaluated every callback a second time (without the context Aspire supplies), executed peers' callbacks on their behalf, and could validate one value while the container received another. A raw WithEnvironment("DATA_PATH", ...), a value supplied as a parameter, or one computed in a callback participates in the read-only, duplicate-mount, and shared-data-directory rules with the documented "last call wins" precedence against WithDataVolume(...)/WithDataBindMount(...), and every comparison runs on canonicalized container paths — previously the guards compared the storage helpers' own target after trimming trailing slashes only, so an alias such as /foo/../data was treated as a different directory from /data even though the runtime mounts both on the same destination, which on images with no data-directory interlock let two resources take one data directory concurrently.
A mount target that reaches above the container root, such as WithVolume("data", "/../data"), is now rejected before the container is created. Docker does not refuse that spelling — it clamps the destination and mounts on /data, which docker inspect confirms and which collides with a plainly spelled /data as Duplicate mount point: /data — so the storage silently lands on a directory the call never named and can become, or collide with, the DocumentDB data directory. It was previously recorded as "escaping" and then ignored, which meant the read-only, duplicate and shared-storage rules never saw it.
A mount on an ancestor of DATA_PATH is now recognised as the mount that backs it. A volume at /data supplies /data/cluster, so with DATA_PATH=/data/cluster that volume is the data mount and the read-only and duplicate-mount rules apply to it; previously only a mount on exactly DATA_PATH counted, and a read-only ancestor was ignored. The most specific mount wins, matched on segment boundaries, so a mount on /data/cluster takes precedence over one on /data and /database is not treated as living under /data. Shared-storage identity now includes the path from the mount target down to DATA_PATH: two resources sharing one volume at /data/alpha and /data/beta are two clusters and are allowed, while two at /data/cluster are one and are refused.
Shared-storage identity is now the directory the cluster occupies rather than the pair of strings it was spelled with. For a bind mount that is one host path — the mount source with whatever part of DATA_PATH falls below the mount target appended — so a resource binding /srv/documentdb and writing to /data/cluster and a resource binding /srv/documentdb/cluster and writing to /data are recognised as sharing one directory, in either declaration order; previously they compared unequal and two PostgreSQL instances could take one directory, silently on images at or below 0.114.0, which have no interlock. The comparison uses the host's own case rules, so /data/Cluster and /data/cluster are one directory on macOS and Windows and two on Linux, matching what is on disk. Bind sources are also canonicalized, so /srv/documentdb, /srv/documentdb/. and /srv/documentdb/../documentdb are one source; symbolic links are deliberately not resolved, because that would depend on the state of the host filesystem at model-build time. A volume keeps its name-plus-subdirectory identity compared exactly: a volume name is not a path, and the container reads that subdirectory on its own case-sensitive filesystem.
The data-storage guard is now installed before AddDocumentDB returns, and takes the last position in the environment and command-line pipelines from every DocumentDB configuration API and at every lifecycle phase, and then verifies it. Installation timing is load-bearing: Aspire records each callback's result the first time a pipeline is gathered and takes the last callback's recording as the answer for the rest of the run, so a callback introduced after a gather does not add to that answer, it replaces it. A guard appended by a lifecycle event therefore dropped USERNAME, PASSWORD and every other value the resource had already produced whenever anything gathered the environment first — which a BeforeStartEvent subscriber registered before AddDocumentDB does simply by calling the public ExecutionConfigurationBuilder. The guard now participates in that very first gather, and the lifecycle phases only move the same callback instances back to the end and attach the services the advisory warnings log to. Anything appended after the last phase, including a lifecycle hook in publish mode where no per-resource event is published, still fails the resource with an explanatory InvalidOperationException instead of letting it start on a data directory nothing checked; so does the mirror image of it — a raw WithEnvironment(...)/WithArgs(...) added after AddDocumentDB and then read by a subscriber registered beforeAddDocumentDB, which gathers the pipeline before any phase can move the guard back, and whose recovery is to register that subscriber after AddDocumentDB. The message describes the shape of the configuration and never repeats the value that displaced the guard. Moving re-uses the same callbacks, so the guarantee of a single evaluation is unchanged.
Publishing a manifest now checks the resource's structure on both sides of the write. Aspire emits a container's image, entrypoint and mounts before it evaluates the environment callbacks and its bindings after them, so a supported WithEnvironment(...) callback could add, remove or replace a mount, re-point an endpoint or swap the image from inside that evaluation: the fields already written described the resource as it was, the rules judged the resource as it became, and a manifest that named neither was published without complaint — a /data volume added that way simply never reached the entry. The mounts (by value, so re-declaring or reordering the same storage is not a change), the endpoints, the membership of the three callback pipelines, the entrypoint, the effective image, the container build definition, the explicit-start setting and the connection string's security state are now recorded immediately before the writer is handed the resource and compared again immediately afterwards. The security state is UseTls(...), AllowInsecureTls(...) and a digest of the exact ConnectionStringExpression.ValueExpression, and it closes a hole of its own: Aspire writes a resource's connectionString before it evaluates a single environment callback, so .WithEnvironment(_ => db.AllowInsecureTls(false)) — a supported call — published tlsInsecure=true while the final model said certificates must be valid, and every deployment reading the manifest was told to skip certificate validation. Each AddDatabase(...) resource carries the same checkpoint, because a database publishes nothing but the connection string its parent builds and is therefore checked where it is written. The build definition is recorded twice over, because the effective image collapses every Dockerfile build to the same answer and the build object is the first thing written: by instance, which catches a definition added, removed or swapped — including one whose Dockerfile factory changed, since a factory cannot be compared by value — and by value, which catches the build arguments, secrets, image name and tag, entry-point flag and generated .dockerignore being set on the annotation that is already there. A build secret is recorded as a digest rather than a value, and no value of any kind reaches the diagnostic. A difference fails the publish, which abandons the partly written manifest rather than completing it, and the message names what kind of thing changed and never repeats a value. Writing environment values from an environment callback is unaffected, resources excluded from the manifest are still excluded, and a caller's own manifest writer still writes the entry it would have written.
A command-line token whose value is only known later — a parameter or a ReferenceExpression — is now rejected wherever the container entrypoint reads an option name, because it could resolve to --data-path and the only way to know would be to resolve it a second time. It is still accepted in the one position where it cannot be an option: directly after a literal option that takes a value, which is the entrypoint's own --option value grammar, so WithArgs("--log-level", level) and WithArgs("--password", password) keep working. Previously only literal strings were examined, so a deferred token could carry --data-path past the guard.
DATA_PATH supplied as a parameter is now rejected in publish mode when the resource also mounts storage. A manifest carries the expression, not a path, so the read-only, duplicate-mount and shared-data-directory rules were all being skipped and a manifest putting two DocumentDB resources on one data directory could be published without complaint. The value is deliberately not resolved — it belongs to the deployment, and a parameter may be a secret — so the configuration is refused instead. A resource that mounts nothing has no storage to get wrong and keeps the expression; run mode resolves the value once and checks it as before.
DATA_PATH is now written into the container environment even when nothing else sets it, using the canonical /data the checks were made against. Leaving it unset let an image whose own default is somewhere else write to a directory the guard never looked at.
The storage guard's advisory warnings — the shared-data-directory downgrade and the declared-image-volume notice — now reach the AppHost log under the Aspire.Hosting.DocumentDB.Storage category. They were being written to the environment callback's own logger, which is not attached during the pass Aspire makes over those callbacks while discovering a container's dependencies; because that pass is the one whose result is cached for the run, every such warning was silently discarded on a real start.
The container entrypoint's --data-path argument (and the reserved -d short form) is now rejected with an actionable InvalidOperationException. The image documents --data-path as "Overrides DATA_PATH environment variable" and the entrypoint exports it while parsing arguments, so a resource that passed it moved its data directory to a path the environment never named, past every storage rule and past the mount that was supposed to back it. The message points at WithDataVolume(), WithDataBindMount(...) and WithEnvironment("DATA_PATH", ...).
An empty DATA_PATH now follows the image default instead of being treated as an error. The entrypoint applies DATA_PATH=${DATA_PATH:-/data}, which treats empty and unset alike, so the guards judge an empty value as /data — and write that canonical value through, so the dashboard and the container agree.
WithLogLevel(...) now sets DOCUMENTDB_LOG_LEVEL, the tracing filter consumed by DocumentDB 0.114.0 and later, making the API observably effective on those images. The legacy LOG_LEVEL value remains because the Local entrypoint validates its six-value contract, not because any Local image uses it to select gateway verbosity; images through 0.113.0 remain verbosity no-ops. Quiet stays mapped to quiet for public API compatibility and becomes newly effective on 0.114.0 and later: upstream has no such tracing level, but currently parses it as an unmatched target that suppresses gateway output, so that behavior depends on upstream filter semantics.
WithInitData(...) and WithoutSampleData() now set INIT_DATA=false together with SKIP_INIT_DATA=true, overriding an earlier INIT_DATA=true and aligning the runtime environment and published manifests with upstream's canonical --skip-init-data behavior. Corrected WithoutUserCreation() guidance to distinguish the default built-in sample initialization in curated images 0.112.0 and older from the opt-in behavior in 0.113.0 and later, while clarifying that WithoutSampleData() does not disable custom initialization.
Percent-encoded the user name and password in the generated mongodb:// and postgresql:// connection strings. Arbitrary credential parameters were interpolated raw into the URI userinfo, so a value containing :, @, /, ?, #, %, a space, or non-ASCII text produced a malformed or misparsed connection string, and a value containing & could inject extra connection options. The registered health checks consume the same expressions, so those values also broke readiness. Encoding uses Aspire's uri reference-expression format, which applies RFC 3986 Uri.EscapeDataString when the expression is resolved, so no secret is read while the application model is built. Credentials made only of unreserved characters — including the default admin user name and the auto-generated password — are still emitted verbatim, leaving existing connection strings byte-identical. The container's USERNAME and PASSWORD environment variables deliberately keep the raw values because the entrypoint consumes them directly.
Corrected troubleshooting guidance that claimed health checks were not registered. The integration uses an authenticated MongoDB ping; documentation now distinguishes gateway availability from completion of the one-shot initialization phase introduced in DocumentDB 0.116.0.
Corrected WithOwner(...) documentation: OWNER names an existing PostgreSQL role used for database operations, not an arbitrary resource label. The bundled image creates the default documentdb role. A missing custom role causes startup to fail: DocumentDB 0.116.0 aborts explicitly during admin-user creation, while earlier images fail later while waiting for the gateway.
Documented that WithDataBindMount(...) does not survive a container restart on Docker Desktop, and recommended WithDataVolume(...) there. PostgreSQL refuses a data directory whose owner is not the user starting the postmaster, and the documentdb-local entrypoint establishes that ownership by running chown on DATA_PATH milliseconds before starting the postmaster. Docker Desktop applies that chown to a bind-mounted host path asynchronously — measured on macOS with VirtioFS, where stat keeps reporting the previous owner for roughly a second, and expected on its Windows and Linux hosts, which share the same file-sharing design — so the restart aborts with FATAL: data directory "/data" has wrong ownership and the data stays on the host but unreadable. The first run is unaffected because initdb runs for several seconds between the two steps. This is a container-runtime limitation rather than a package defect: it reproduces with a plain docker run, and pg17-0.114.0 behaves identically (a fresh first run on an empty bind mount succeeds and becomes ready; a restart against that initialized bind-mounted data fails with the same ownership error), so it is not a DocumentDB 0.116.0 regression. WithDataVolume(...) is unaffected because a named volume lives inside the container runtime's own filesystem, and a bind mount on a native container engine is an ordinary mount that restarts normally.
Preserved WithOpenTelemetryMetrics(...) behavior for DocumentDB 0.116.0 and later in both direct AppHost run mode and every publisher, including aspire publish and azd. Upstream 0.116.0 made the gateway resolve telemetry as JSON > environment > default and shipped a SetupConfiguration.json that pins metrics off, so the documented OTEL_* variables no longer took effect. The integration now wraps the container entrypoint for the official image at that version or later. The wrapper resolves the configuration directory exactly as the image entrypoint does (CONFIG_DIR, then the packaged /etc/documentdb/gateway layout, then $GATEWAY_HOME/pg_documentdb_gw), removes the TelemetryOptions.Metrics object whole — this API owns that signal, the shipped OtlpEndpoint would otherwise beat both OTEL_EXPORTER_OTLP_METRICS_ENDPOINT and OTEL_EXPORTER_OTLP_ENDPOINT, and deleting today's keys individually would leave any field a future gateway release adds authoritative over the environment — plus the shared ServiceName/ServiceVersion only when the corresponding override was explicitly supplied, then repoints CONFIG_DIR and execs the image's own entrypoint. TelemetryOptions.Tracing and the shipped service identity are left in place, and no default identity is injected. enabled: false installs the wrapper as well, so it beats a configuration file that enables metrics from JSON. Because the wrapper is carried in the container entrypoint and args, the published manifest still names the official image and remains deployable. Custom images and tags outside the pgNN-X.Y.Z grammar are untouched; private registry mirrors of the official image are covered. Digest-pinned official images, caller-supplied entrypoints, and an entrypoint replaced after the wrapper installed one — including by a later BeforeStartEvent subscriber in the same startup, which the argument callback re-checks for — all fail with actionable errors rather than silently skipping or over-applying the override. The image is evaluated at start/publish time, so selecting the version after calling WithOpenTelemetryMetrics(...) works.
Every version-dependent decision in a run is now judged on the image the container runtime is actually committed to, rather than on the image annotation as it reads at the moment the decision runs. Aspire snapshots a container's image into the orchestrator's container spec while it prepares resources — one TryGetContainerImageName call per resource, before endpoints are allocated — and nothing re-reads the annotation for it afterwards, so ResourceEndpointsAllocatedEvent, BeforeResourceStartedEvent and the container-runtime-argument callbacks all run after the image is fixed. Changing the tag from any of them moved every rule in this package onto a release the container is not running: raising it (pg17-0.111.0 to pg17-0.116.0) made the WithPostgresEndpoint() credential floor and the Pg18 variant floor pass on an image that does not satisfy them, and lowering it (pg17-0.116.0 to pg17-0.114.0) made the shared-data-directory rule promise a data-directory flock the running image does not have and downgrade a hard failure to a warning on the strength of it. A run now seals the composed image reference, the resolved release and the DockerfileBuildAnnotation identities from an IDistributedApplicationLifecycleHook.BeforeStartAsync — which Aspire runs after every BeforeStartEvent subscriber, including ones registered after AddDocumentDB, and still before the orchestrator prepares anything — and every version floor, telemetry eligibility decision and storage lock assumption reads the seal. One hook seals the whole model, however many DocumentDB resources it has. A later change is refused with an InvalidOperationException naming both references, at the uncached container-runtime-arguments checkpoint and at the resource-start event. Ordinary configuration is untouched: the image may be chosen in any order while the model is built and from a BeforeStartEvent subscriber, custom repositories, unrecognised tags, digest pins and Dockerfile builds keep their existing carve-outs, and a publish takes no seal at all, because no container is ever prepared and the manifest is written from the model at serialization time.
Container-runtime arguments are now read once, with the union of the Docker and Podman run option grammars, and refused when they would reach past the application model. Which runtime is behind them is not this package's to know, so both are modelled: WithContainerRuntimeArgs(...) passes its arguments straight to the runtime ahead of the image, and nothing it adds is visible to any rule written against the model. Storage is refused whether or not it spells a mount — --mount, -v, --volume, --volumes-from, --tmpfs, --read-only, --volume-driver, --storage-opt, --chrootdirs, --ipc, --pod, --pod-id-file, --read-only-tmpfs, --systemd, --use-api-socket and Podman's --secret, --image-volume and --init-path all create, select or alter the storage the data directory sits on with no ContainerMountAnnotation to read. --env/-e could replace DATA_PATH, USERNAME or PASSWORD out from under the generated connection string, and so could Podman's --env-merge and --unsetenv, which are refused exactly when the variable they name is one this package owns; --env-file, --env-host and --unsetenv-all reach environment this package cannot read from the token and are refused outright. --entrypoint replaced the entry point without touching the model, and a bare operand — or Podman's --rootfs, which makes the operand a directory instead of an image — would have displaced the sealed image. There is one parser and one terminal container-runtime callback for the whole package, so the storage rules and the WithOpenTelemetryMetrics(...) wrapper judge the same finished line with their own idea of which variables they own, instead of competing for the last position. The option=value and split forms, the attached short form (-eDATA_PATH=...), short-option clusters (-itv ...) and values contributed by another callback are all covered, and a token whose value is only known later is refused where the runtime reads an option name — the same rule the entrypoint's own arguments already followed. A value-less option takes its value only after an =, which is what the flag parser both runtimes use does, so --systemd --mount=type=bind,... can no longer swallow the mount as an operand; and an explicit off value on one of them — --read-only=false, --read-only-tmpfs=false, --use-api-socket=false, --env-host=false, --systemd=false — is the caller declining it and passes through. While the wrapper is required the list is resolved exactly once and written back, so the strings validated are the strings the runtime receives, an option neither runtime documents is refused rather than read as a flag, and a missing required value is refused too. This is a parser, not a search: --label -v passes a label, --memory 512m is not a mount, and --cap-add, --network, --pull, --platform, --dns, --ulimit, --sysctl, -it and the Podman-only --tz, --umask, --sdnotify, --uidmap, --authfile, --seccomp-policy and the rest reach the container runtime untouched.
No value from a container-runtime argument reaches a diagnostic, whether it is a literal, a parameter resolved later, or the error a resolution raised. A refused argument is reported by option spelling alone — taken from this package's own table rather than from the token that was read — plus, for an environment option, the package-owned variable name this package itself chose. Operands are never reported, and neither is a bare positional token: that is where a caller who resolved a connection string themselves would have written it, and a secret ParameterResource or a credential-bearing ReferenceExpression standing in the same position is read by position only. An option neither runtime documents, a missing required value and a deferred value whose resolution threw are each reported by kind alone, and the original exception is deliberately not chained. Previously a positional operand and an unrecognised option were carried into the failure verbatim.
The manifest checkpoints for a DocumentDB server and its databases are now put back in the position the publisher reads at a phase no event subscriber can be registered after. Aspire publishes BeforePublishEvent to every subscriber and only then executes the publishing pipeline, so a subscriber registered after this package's — from an app host, or from a lifecycle hook, both ordinary — could call WithManifestPublishingCallback(...) on a resource after this package had taken the last position back. The publisher reads the last annotation and no other, so the checkpoint never ran at all: the entry was written by a callback free to change the parent's TLS settings halfway through, and a manifest saying tlsInsecure=true while the model said certificates must be valid was published without complaint. Every checkpoint is now recorded on its resource and restored from a single publishing-pipeline configuration callback, which Aspire runs when it resolves the pipeline's steps — after the last subscriber and before the step that writes the manifest. A caller's own writer still writes the entry it would have written, and ExcludeFromManifest() still excludes the resource; the per-event retakes are kept as well.
A registry changed while a DocumentDB server's manifest entry is being written is now refused instead of being published inverted. Aspire's container writer emits image from TryGetContainerImageName before it evaluates a single environment callback, and the checkpoint recorded only what this package knows about the release — repository, tag, digest, version — which deliberately says nothing about which host the image is mirrored on, because a mirror of the curated repository is the same release. A supported db.WithEnvironment(_ => db.WithImageRegistry("mirror.example")), or a caller's own WithManifestPublishingCallback(...) that the checkpoint delegates to, therefore left every recorded field identical while the entry already carried ghcr.io/documentdb/...: the published manifest sent every deployment to the registry the resource was configured with rather than the one it ended up naming, and nothing reported it. The checkpoint now also records the exact composed reference — the same string Aspire writes, registry/ prefix, :tag suffix and @sha256: form included — and fails the publish when it changes, whether the change is registry-only, the repository, the tag or any other part of the reference. Nothing is repaired: the field is already in the writer, so the publish fails and the partly written manifest is abandoned rather than completed. Container builds the caller owns are unaffected, because their entry carries build and no image at all and the complete build-definition snapshot is what protects them; the run-mode image seal, which already compared the composed reference, is unchanged. The diagnostic names the kind of change and no reference, tag or credential.
Bind-backed telemetry scratch candidates are now conservative across Docker daemon boundaries. A bind source is resolved by the daemon or publish host and may traverse symbolic links unavailable on the machine building the model, so two lexically different source paths cannot prove physical disjointness. Whenever DATA_PATH and a candidate are both bind-backed, that candidate is skipped and the wrapper tries /tmp, /var/tmp, then /dev/shm; it fails clearly when every candidate aliases the data directory or cannot be proven independent. Named volumes and their subpaths are still compared exactly, a mounted alias now also rules out every container directory above it, different storage types stay usable, raw runtime mounts are rejected before container creation because they are absent from the mount table, and the generated command — and therefore the published manifest — no longer depends on local host-path or symlink resolution.
ContainerResource.ShellExecution set to true is now rejected whenever the WithOpenTelemetryMetrics(...) compatibility wrapper is required. Aspire's orchestrator applies that switch after the terminal argument callback and replaces the verified -c <script> -- ... list with -c "<joined arguments>", which handed /bin/bash a nested -c and prevented DocumentDB from starting. The wrapper now requires ShellExecution to stay null or false, records its effective value in the terminal seal, and re-checks it at the uncached container-runtime and manifest checkpoints, including after an early configuration read. Resources that do not need the wrapper — custom images, unrecognised tags, and official images below the gateway-telemetry floor — keep their existing shell-execution behavior.