Skip to content
Open
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
32 changes: 32 additions & 0 deletions cmd/compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,13 +298,45 @@ func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, ser
return nil, err
}

if err := checkConfigPathsNotDirectories(options.ConfigPaths, remotes); err != nil {
return nil, err
}

if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
api.Separator = "_"
}

return options.LoadModel(ctx)
}

// checkConfigPathsNotDirectories returns an error if any local config path is a
// directory rather than a file. Remote resource paths and stdin ("-") are skipped.
//
// This guards against COMPOSE_FILE being set to a directory (e.g. COMPOSE_FILE=""
// which filepath.Abs resolves to the working directory).
func checkConfigPathsNotDirectories(configPaths []string, remoteLoaders []loader.ResourceLoader) error {
for _, configPath := range configPaths {
if configPath == "-" {
continue
}
isRemote := false
for _, r := range remoteLoaders {
if r.Accept(configPath) {
isRemote = true
break
}
}
if isRemote {
continue
}
if info, err := os.Stat(configPath); err == nil && info.IsDir() {
return fmt.Errorf("path %q is a directory, not a Compose file; "+
"check the COMPOSE_FILE environment variable or the -f flag", configPath)
}
}
return nil
}

// ToProject loads a Compose project using the LoadProject API.
// Accepts optional cli.ProjectOptionsFn to control loader behavior.
func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) {
Expand Down
35 changes: 35 additions & 0 deletions pkg/compose/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package compose
import (
"context"
"errors"
"fmt"
"os"
"strings"

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

// checkConfigPathsForDirectories returns an error if any config path in configPaths
// is a local directory instead of a file. Remote paths (accepted by remoteLoaders)
// and the special "-" (stdin) value are skipped.
//
// This provides a clear error when COMPOSE_FILE is set to a directory path (e.g.,
// "COMPOSE_FILE=" resolves to the working directory via filepath.Abs("")).
func checkConfigPathsForDirectories(configPaths []string, remoteLoaders []loader.ResourceLoader) error {
for _, configPath := range configPaths {
if configPath == "-" {
continue
}
isRemote := false
for _, r := range remoteLoaders {
if r.Accept(configPath) {
isRemote = true
break
}
}
if isRemote {
continue
}
info, err := os.Stat(configPath)
if err == nil && info.IsDir() {
return fmt.Errorf("path %q is a directory, not a Compose file; "+
"check the COMPOSE_FILE environment variable or the -f flag", configPath)
}
}
return nil
}

// LoadProject implements api.Compose.LoadProject
// It loads and validates a Compose project from configuration files.
func (s *composeService) LoadProject(ctx context.Context, options api.ProjectLoadOptions) (*types.Project, error) {
Expand All @@ -42,6 +73,10 @@ func (s *composeService) LoadProject(ctx context.Context, options api.ProjectLoa
return nil, err
}

if err := checkConfigPathsForDirectories(projectOptions.ConfigPaths, remoteLoaders); err != nil {
return nil, err
}

// Register all user-provided listeners (e.g., for metrics collection)
for _, listener := range options.LoadListeners {
if listener != nil {
Expand Down
22 changes: 22 additions & 0 deletions pkg/compose/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,25 @@ func TestLoadProject_MissingComposeFile(t *testing.T) {
require.Error(t, err)
assert.Nil(t, project)
}

func TestLoadProject_DirectoryAsComposeFile(t *testing.T) {
// Reproduce the misleading error described in https://github.com/docker/compose/issues/13649:
// when COMPOSE_FILE is set to a directory (e.g. COMPOSE_FILE="" resolves to the working
// directory via filepath.Abs("")), the error "read <dir>: is a directory" was shown.
// The fix should return a clear error message instead.
tmpDir := t.TempDir()

service, err := NewComposeService(nil)
require.NoError(t, err)

project, err := service.LoadProject(t.Context(), api.ProjectLoadOptions{
ConfigPaths: []string{tmpDir},
})

require.Error(t, err)
assert.Nil(t, project)
assert.Contains(t, err.Error(), "is a directory")
assert.Contains(t, err.Error(), "Compose file")
// Ensure the old opaque error message is NOT present
assert.NotContains(t, err.Error(), "read "+tmpDir+": is a directory")
}