This commit is contained in:
Khizar Ali 2026-05-12 10:12:13 +00:00 committed by GitHub
commit e30fc4e8ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 89 additions and 0 deletions

View file

@ -298,6 +298,10 @@ 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 = "_"
}
@ -305,6 +309,34 @@ func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, ser
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) {

View file

@ -19,6 +19,7 @@ package compose
import (
"context"
"errors"
"fmt"
"os"
"strings"
@ -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) {
@ -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 {

View file

@ -319,3 +319,25 @@ func TestLoadProject_MissingComposeFile(t *testing.T) {
assert.Assert(t, err != nil)
assert.Assert(t, project == nil)
}
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")
}