Repository navigation
fix(compose): reattach existing projects without fetching features - #144
Merged
Merged
Conversation
Every compose-source Up ran createFreshCompose -> layerFeatures, which
fetched each feature from its registry and rebuilt dc-go-final-<id>
even when the project's containers already existed. A restart of a
compose workspace with features therefore needed registry egress and
paid a final-image rebuild; without egress (a docker-in-docker
workspace in a guest with no route to ghcr.io) it could not start at
all. The image path's attachExisting never had this cost.
- Stamp dev.containers.compose-inputs-hash on the primary container:
a digest of the container-shaping part of the resolved config
(features as requested, with options; local feature content), the
loaded compose project, the primary base image ID and the engine
options that reshape the project. On a later Up with a matching
label and an unmoved image tag, reattachCompose merges the metadata
from the existing image's label and brings the project up against
the existing images (native under the caller's AdoptExisting with
present sidecar images not rebuilt; shell-out with --no-recreate).
Any miss takes the full path unchanged.
- Escape $ in the devcontainer.metadata LABEL: the Dockerfile parser
expanded ${devcontainerId}, so docker-in-docker's volume source was
recorded as "dind-var-lib-docker-" and every read of the label lost
the per-workspace volume name.
- OCI feature cache is local-first for digest refs and exact X.Y.Z
tags; floating tags stay online-first and fall back to the recorded
resolution only when the registry is unreachable. Extraction is
atomic. Adds DiskStoreOptions.OCITransport and
ContainerDetails.ImageRef.
Cut 0.5.2 in the changelog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMTBosSzdEvXqCZ9c7VyLk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
For compose-source devcontainers,
Upalways rancreateFreshCompose→layerFeatures. It fetched every feature from its registry and rebuiltdc-go-final-<id>, even when the project's containers already existed from a previous run. The image path'sattachExistinginstead reuses the existing container and reads the merged metadata from its image'sdevcontainer.metadatalabel, with no network. The OCI feature cache also asked the registry for the manifest before it checked itself.Consequences:
fetch feature ghcr.io/devcontainers/features/docker-in-docker:2.17.0: … connect: connection refused.Fix
1. Compose reattach (
up_compose_reattach.go)Label. The full path stamps
dev.containers.compose-inputs-hash(LabelComposeInputsHash) on the primary container. It is a sha256 over the inputs that are fixed before any feature is fetched:devcontainer.json: workspace folder and mount, mounts, users,updateRemoteUserUID,containerEnv,runArgs, init, privileged, capAdd, securityOpt, overrideCommand, and the features as requested with their options, plus a content digest of each local feature;ComposeBackend,DisableHostPortPublishingandStrictFeatureVersionMatch;Lifecycle commands,
remoteEnv, ports attributes, customizations and the name are left out.Upre-reads them on every call and they never reach the container or its image. The hash also has a version field.Reattach. On an
Upwith an existing container, unlessRecreate,PullAlwaysor anEngine.Buildoverride applies, the engine finds the primary container by project, service and devcontainer id. It reattaches when:ContainerDetails.ImageRef, new) still names the image the container runs.It then merges the image label into cfg as
attachExistingdoes. It rebuilds the run override through the same helper as the full path: entrypoint wrapper, all mount types including the fix(compose): carry volume and tmpfs mounts onto the primary service #143 volume/tmpfs fix, env, privileged, capAdd and labels. Finally it brings the project up against the existing images:Orchestrator.Upunder the caller'sAdoptExisting. Sidecar builds are skipped when their image is present.docker compose up -d --no-recreate.No feature is fetched and no image is built. Because the image is unchanged, the orchestrator's per-service config hash also matches, so a native
UpwithoutAdoptExistingreuses the containers. The integration test checks the container IDs.Miss. No container, no label, a different hash, a moved tag,
RecreateorPullAlwaysall take the full path, unchanged.cfgis not touched on a miss.AdoptExistingkeeps its meaning: reuse a container whose config drifted. Reattach only removes the fetch and the rebuild in front of it.Upgrade. Containers created before this change have no label, so they take the full path. Native without
AdoptExistingrecreates them on the firstUp, which adds the label. WithAdoptExisting, or on shell-out, they are kept unlabelled until they are recreated.2. Metadata label
$escaping (found by the new test)GenerateBuildContextwrote thedevcontainer.metadataLABEL withstrconv.Quoteonly. The Dockerfile parser expands${…}inside double quotes, so docker-in-docker'sdind-var-lib-docker-${devcontainerId}was stored asdind-var-lib-docker-. A reattach that read the label therefore mounted a different, empty volume. The image path'sattachExistinghas the same latent issue inws.Config.Mounts. Every$is now escaped (dockerfileLiteral).3. Feature cache policy (
feature/oci.go,design/features.md§8.1)name@sha256:…name:X.Y.Zname:X,name:X.Y,latest, other tagsoci/tags/and are written atomically.DiskStoreOptions.OCITransport.The changelog cuts 0.5.2. The change only adds API, following the v0.4.3/v0.5.1 precedent.
Tests
up_compose_reattach_test.go):containerEnv, mounts, the primary service, a sidecar service, an engine option, and local feature content;remoteEnv, ports and customizations;PullAlways. A control case commits.feature/oci_cache_test.go, in-process registry with a gated transport):\${devcontainerId}.test/integration/compose_reattach_test.go). An in-process registry serves a small feature that declares env, capAdd, a volume, an entrypoint and postStartCommand.RestartsWithoutRegistry, for native, native+adopt, shellout and shellout+adopt. Up, stop every service, block the registry, up again:The first Up is checked unchanged: 1 fetch, 1 build, label present.
ConfigChangeRebuilds: after a feature option change, the next Up fetches and builds again. Native recreates with a new label and the new option.TestComposeSource_DockerInDockerRestartsWithoutRegistry, both backends, with the realdocker-in-docker:2:docker pull hello-worldin the inner dockerd;/var/lib/dockerstill a volume;docker run --pull never hello-worldprints "Hello from Docker!".Local results (Linux, Docker 29.1.3, Compose 2.40.3, Go 1.25.3):
make lint(golangci-lint v2.5.0): 0 issues.go vet ./...passes, including-tags=integration.make testpasses.go test -race -tags=integration ./test/integration/..., 839 s.🤖 Generated with Claude Code
https://claude.ai/code/session_01UMTBosSzdEvXqCZ9c7VyLk