Skip to content

Commit 2164493

Browse files
committed
fix: return clear error when COMPOSE_FILE points to a directory
When COMPOSE_FILE is set to an empty string or a path that resolves to a directory (e.g. COMPOSE_FILE="" resolves to cwd via filepath.Abs), the previous behaviour was to emit a cryptic OS-level error: read <path>: is a directory This occurs because compose-go's ReadConfigFiles calls os.ReadFile on the resolved path, which fails with EISDIR when the path is a directory. Add an explicit pre-check in both LoadProject (pkg/compose) and ToModel (cmd/compose) that detects when a resolved config path is a directory and returns a user-friendly message: path "<path>" is a directory, not a Compose file; check the COMPOSE_FILE environment variable or the -f flag Remote paths (git/OCI) and stdin ("-") are excluded from the check. Fixes #13649 Made-with: Cursor
1 parent ef86a6e commit 2164493

File tree

3 files changed

+89
-0
lines changed

3 files changed

+89
-0
lines changed

cmd/compose/compose.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,13 +298,45 @@ func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, ser
298298
return nil, err
299299
}
300300

301+
if err := checkConfigPathsNotDirectories(options.ConfigPaths, remotes); err != nil {
302+
return nil, err
303+
}
304+
301305
if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
302306
api.Separator = "_"
303307
}
304308

305309
return options.LoadModel(ctx)
306310
}
307311

312+
// checkConfigPathsNotDirectories returns an error if any local config path is a
313+
// directory rather than a file. Remote resource paths and stdin ("-") are skipped.
314+
//
315+
// This guards against COMPOSE_FILE being set to a directory (e.g. COMPOSE_FILE=""
316+
// which filepath.Abs resolves to the working directory).
317+
func checkConfigPathsNotDirectories(configPaths []string, remoteLoaders []loader.ResourceLoader) error {
318+
for _, configPath := range configPaths {
319+
if configPath == "-" {
320+
continue
321+
}
322+
isRemote := false
323+
for _, r := range remoteLoaders {
324+
if r.Accept(configPath) {
325+
isRemote = true
326+
break
327+
}
328+
}
329+
if isRemote {
330+
continue
331+
}
332+
if info, err := os.Stat(configPath); err == nil && info.IsDir() {
333+
return fmt.Errorf("path %q is a directory, not a Compose file; "+
334+
"check the COMPOSE_FILE environment variable or the -f flag", configPath)
335+
}
336+
}
337+
return nil
338+
}
339+
308340
// ToProject loads a Compose project using the LoadProject API.
309341
// Accepts optional cli.ProjectOptionsFn to control loader behavior.
310342
func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) {

pkg/compose/loader.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package compose
1919
import (
2020
"context"
2121
"errors"
22+
"fmt"
2223
"os"
2324
"strings"
2425

@@ -31,6 +32,36 @@ import (
3132
"github.com/docker/compose/v5/pkg/utils"
3233
)
3334

35+
// checkConfigPathsForDirectories returns an error if any config path in configPaths
36+
// is a local directory instead of a file. Remote paths (accepted by remoteLoaders)
37+
// and the special "-" (stdin) value are skipped.
38+
//
39+
// This provides a clear error when COMPOSE_FILE is set to a directory path (e.g.,
40+
// "COMPOSE_FILE=" resolves to the working directory via filepath.Abs("")).
41+
func checkConfigPathsForDirectories(configPaths []string, remoteLoaders []loader.ResourceLoader) error {
42+
for _, configPath := range configPaths {
43+
if configPath == "-" {
44+
continue
45+
}
46+
isRemote := false
47+
for _, r := range remoteLoaders {
48+
if r.Accept(configPath) {
49+
isRemote = true
50+
break
51+
}
52+
}
53+
if isRemote {
54+
continue
55+
}
56+
info, err := os.Stat(configPath)
57+
if err == nil && info.IsDir() {
58+
return fmt.Errorf("path %q is a directory, not a Compose file; "+
59+
"check the COMPOSE_FILE environment variable or the -f flag", configPath)
60+
}
61+
}
62+
return nil
63+
}
64+
3465
// LoadProject implements api.Compose.LoadProject
3566
// It loads and validates a Compose project from configuration files.
3667
func (s *composeService) LoadProject(ctx context.Context, options api.ProjectLoadOptions) (*types.Project, error) {
@@ -42,6 +73,10 @@ func (s *composeService) LoadProject(ctx context.Context, options api.ProjectLoa
4273
return nil, err
4374
}
4475

76+
if err := checkConfigPathsForDirectories(projectOptions.ConfigPaths, remoteLoaders); err != nil {
77+
return nil, err
78+
}
79+
4580
// Register all user-provided listeners (e.g., for metrics collection)
4681
for _, listener := range options.LoadListeners {
4782
if listener != nil {

pkg/compose/loader_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,25 @@ func TestLoadProject_MissingComposeFile(t *testing.T) {
320320
require.Error(t, err)
321321
assert.Nil(t, project)
322322
}
323+
324+
func TestLoadProject_DirectoryAsComposeFile(t *testing.T) {
325+
// Reproduce the misleading error described in https://github.com/docker/compose/issues/13649:
326+
// when COMPOSE_FILE is set to a directory (e.g. COMPOSE_FILE="" resolves to the working
327+
// directory via filepath.Abs("")), the error "read <dir>: is a directory" was shown.
328+
// The fix should return a clear error message instead.
329+
tmpDir := t.TempDir()
330+
331+
service, err := NewComposeService(nil)
332+
require.NoError(t, err)
333+
334+
project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
335+
ConfigPaths: []string{tmpDir},
336+
})
337+
338+
require.Error(t, err)
339+
assert.Nil(t, project)
340+
assert.Contains(t, err.Error(), "is a directory")
341+
assert.Contains(t, err.Error(), "Compose file")
342+
// Ensure the old opaque error message is NOT present
343+
assert.NotContains(t, err.Error(), "read "+tmpDir+": is a directory")
344+
}

0 commit comments

Comments
 (0)