Skip to content

Commit 619986b

Browse files
aledbfclaude
andcommitted
fix(docker): let compose builds read the Features Dockerfile
Building a compose configuration now goes through `docker compose build`, as in TS. Compose builds through bake, and bake on buildx 0.38 and later refuses the Features Dockerfile, written under the cache folder outside the build context, unless --allow=fs.read is passed, which compose cannot do. TestE2E_ComposeBuildLabel failed that way on the runners. The compose processes this CLI starts get BUILDX_BAKE_ENTITLEMENTS_FS=0 unless the variable is already set. The reference fails there; docs/DIVERGENCES.md records it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e92f40b commit 619986b

3 files changed

Lines changed: 49 additions & 1 deletion

File tree

‎docs/DIVERGENCES.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ touches a compared surface, reflected in the parity matrix.
2727
Dockerfile as it is (`TestEnsureFinalStageNameReadsAContinuedFrom`,
2828
`internal/docker/testdata/ts-oracle/dockerfile.json`). Every other FROM line is
2929
handled exactly as TS handles it.
30+
- **Compose builds may read the Features Dockerfile outside the build context.** Compose
31+
builds through bake, and bake on buildx 0.38 and later refuses to read a file outside the
32+
context unless the caller passes `--allow=fs.read`, which `docker compose build` cannot.
33+
The reference writes the Features Dockerfile under its cache folder and fails there with
34+
"additional privileges requested". This CLI starts its `docker compose` processes with
35+
`BUILDX_BAKE_ENTITLEMENTS_FS=0` unless the variable is already set
36+
(`TestWithBakeFSEntitlement`, `TestE2E_ComposeBuildLabel`).
3037
- **`--override-config` deep-merges** the override onto the base config, whereas TS
3138
replaces the config wholesale (`readDocument(overrideConfigFile ?? configFile)`). With
3239
no readable base, the override stands alone — identical to TS. This lets an orchestrator

‎internal/docker/compose.go‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8+
"os"
89
osexec "os/exec"
910
"slices"
1011
"strconv"
@@ -45,7 +46,7 @@ func NewComposeClient(dockerPath, composePath string, env []string, logger log.L
4546
return &ComposeClient{
4647
Command: []string{dockerPath, "compose"},
4748
Version: parseComposeVersion(strings.TrimSpace(string(out))),
48-
Env: env,
49+
Env: withBakeFSEntitlement(env, os.LookupEnv),
4950
Log: logger,
5051
}, nil
5152
}
@@ -55,6 +56,24 @@ func NewComposeClient(dockerPath, composePath string, env []string, logger log.L
5556
return nil, fmt.Errorf("'docker compose' (v2) not found")
5657
}
5758

59+
// bakeFSEntitlement turns off bake's filesystem entitlement check for the
60+
// compose processes this CLI starts. Compose builds through bake, and bake on
61+
// buildx 0.38 and later refuses to read a Dockerfile outside the build context
62+
// unless the caller passes --allow=fs.read, which `docker compose build` cannot;
63+
// the Features Dockerfile this CLI writes lives in its cache folder, outside the
64+
// context. The reference passes nothing and fails there ("additional privileges
65+
// requested"); docs/DIVERGENCES.md records the difference.
66+
const bakeFSEntitlement = "BUILDX_BAKE_ENTITLEMENTS_FS"
67+
68+
// withBakeFSEntitlement is env plus BUILDX_BAKE_ENTITLEMENTS_FS=0, unless env or
69+
// the process environment (lookup) already sets it.
70+
func withBakeFSEntitlement(env []string, lookup func(string) (string, bool)) []string {
71+
if _, ok := lookup(bakeFSEntitlement); ok || slices.ContainsFunc(env, func(kv string) bool { return strings.HasPrefix(kv, bakeFSEntitlement+"=") }) {
72+
return env
73+
}
74+
return append(slices.Clone(env), bakeFSEntitlement+"=0")
75+
}
76+
5877
// Run executes a compose command and captures output (no live streaming).
5978
func (c *ComposeClient) Run(ctx context.Context, args ...string) (*ExecResult, error) {
6079
return c.run(ctx, false, args...)

‎internal/docker/compose_test.go‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package docker
22

33
import (
4+
"slices"
45
"testing"
56
)
67

@@ -135,3 +136,24 @@ func TestBuildGlobalArgs_NoEnvFile(t *testing.T) {
135136
t.Errorf("args = %v", args)
136137
}
137138
}
139+
140+
// The compose processes get BUILDX_BAKE_ENTITLEMENTS_FS=0 so bake reads the
141+
// Features Dockerfile outside the build context, unless the user set it.
142+
func TestWithBakeFSEntitlement(t *testing.T) {
143+
unset := func(string) (string, bool) { return "", false }
144+
if got := withBakeFSEntitlement([]string{"A=1"}, unset); !slices.Equal(got, []string{"A=1", "BUILDX_BAKE_ENTITLEMENTS_FS=0"}) {
145+
t.Errorf("unset: %q", got)
146+
}
147+
if got := withBakeFSEntitlement(nil, func(k string) (string, bool) { return "1", k == "BUILDX_BAKE_ENTITLEMENTS_FS" }); len(got) != 0 {
148+
t.Errorf("set in the process: %q, want it left alone", got)
149+
}
150+
if got := withBakeFSEntitlement([]string{"BUILDX_BAKE_ENTITLEMENTS_FS=1"}, unset); !slices.Equal(got, []string{"BUILDX_BAKE_ENTITLEMENTS_FS=1"}) {
151+
t.Errorf("set in env: %q", got)
152+
}
153+
env := make([]string, 1, 4)
154+
env[0] = "A=1"
155+
withBakeFSEntitlement(env, unset)
156+
if env[:2][1] != "" {
157+
t.Error("the caller's env was written to")
158+
}
159+
}

0 commit comments

Comments
 (0)