Skip to content
Merged
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
10 changes: 9 additions & 1 deletion cli/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,15 @@ PATH:

// ReadConfigFiles reads ConfigFiles and populates the content field
func (o *ProjectOptions) ReadConfigFiles(ctx context.Context, workingDir string, options *ProjectOptions) (*types.ConfigDetails, error) {
config, err := loader.LoadConfigFiles(ctx, options.ConfigPaths, workingDir, options.loadOptions...)
// workingDir already resolved options.WorkingDir with precedence over any
// default (see GetWorkingDir): pass that precedence down explicitly, so a
// remote resource loader (git, oci) does not override it with the
// directory of its own downloaded copy.
explicit := options.WorkingDir != ""
loadOptions := append(append([]func(*loader.Options){}, options.loadOptions...), func(o *loader.Options) {
o.SetWorkingDirExplicit(explicit)
})
config, err := loader.LoadConfigFiles(ctx, options.ConfigPaths, workingDir, loadOptions...)
if err != nil {
return nil, err
}
Expand Down
77 changes: 77 additions & 0 deletions loader/load_config_files_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
Copyright 2020 The Compose Specification Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package loader

import (
"context"
"os"
"path/filepath"
"strings"
"testing"

"gotest.tools/v3/assert"
)

// fakeRemoteLoader simulates a remote resource loader (git, oci): it hands
// back a path under its own "download" directory, exactly like a real
// remote loader returns the local copy it fetched the artifact into.
type fakeRemoteLoader struct {
downloadDir string
}

func (l fakeRemoteLoader) Accept(path string) bool {
return strings.HasPrefix(path, "remote://")
}

func (l fakeRemoteLoader) Load(_ context.Context, _ string) (string, error) {
return filepath.Join(l.downloadDir, "compose.yaml"), nil
}

func (l fakeRemoteLoader) Dir(_ string) string {
return l.downloadDir
}

// TestLoadConfigFilesHonorsExplicitWorkingDir is the docker/compose#14224
// repro at the compose-go layer: an explicit working dir (--project-directory)
// must survive a remote resource loader, which otherwise defaults the
// working dir to its own downloaded copy's directory so a self-contained
// remote artifact (extends, bundled env files) resolves against itself.
// That default must never override an explicit request.
func TestLoadConfigFilesHonorsExplicitWorkingDir(t *testing.T) {
downloadDir := t.TempDir()
assert.NilError(t, os.WriteFile(filepath.Join(downloadDir, "compose.yaml"), []byte("services: {}"), 0o600))
remote := fakeRemoteLoader{downloadDir: downloadDir}

t.Run("explicit working dir is preserved", func(t *testing.T) {
explicitDir := t.TempDir()
config, err := LoadConfigFiles(context.Background(), []string{"remote://ref"}, explicitDir,
func(o *Options) { o.ResourceLoaders = []ResourceLoader{remote} },
func(o *Options) { o.SetWorkingDirExplicit(true) },
)
assert.NilError(t, err)
assert.Equal(t, config.WorkingDir, explicitDir)
})

t.Run("defaulted working dir still falls back to the downloaded copy's directory", func(t *testing.T) {
defaultedDir := t.TempDir()
config, err := LoadConfigFiles(context.Background(), []string{"remote://ref"}, defaultedDir,
func(o *Options) { o.ResourceLoaders = []ResourceLoader{remote} },
)
assert.NilError(t, err)
assert.Equal(t, config.WorkingDir, downloadDir)
})
}
31 changes: 23 additions & 8 deletions loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,13 @@ type Options struct {
discardEnvFiles bool
// Set project projectName
projectName string
// Indicates when the projectName was imperatively set or guessed from path
projectNameImperativelySet bool
// Indicates when the projectName was explicitly set or guessed from path
projectNameExplicit bool
// Indicates the working dir passed to LoadConfigFiles was explicitly
// requested (e.g. --project-directory) rather than defaulted (e.g. the
// current directory), so a remote resource loader (git, oci) must not
// override it with the downloaded copy's own directory
workingDirExplicit bool
// Profiles set profiles to enable
Profiles []string
// SelectedServices restricts the project model to these services (and their dependencies)
Expand Down Expand Up @@ -208,7 +213,8 @@ func (o *Options) clone() *Options {
Interpolate: o.Interpolate,
discardEnvFiles: o.discardEnvFiles,
projectName: o.projectName,
projectNameImperativelySet: o.projectNameImperativelySet,
projectNameExplicit: o.projectNameExplicit,
workingDirExplicit: o.workingDirExplicit,
Profiles: o.Profiles,
SelectedServices: o.SelectedServices,
PruneUnnecessaryResources: o.PruneUnnecessaryResources,
Expand All @@ -219,13 +225,22 @@ func (o *Options) clone() *Options {
}
}

func (o *Options) SetProjectName(name string, imperativelySet bool) {
func (o *Options) SetProjectName(name string, explicit bool) {
o.projectName = name
o.projectNameImperativelySet = imperativelySet
o.projectNameExplicit = explicit
}

func (o Options) GetProjectName() (string, bool) {
return o.projectName, o.projectNameImperativelySet
return o.projectName, o.projectNameExplicit
}

// SetWorkingDirExplicit records whether the working dir passed to
// LoadConfigFiles was explicitly requested by the caller (e.g.
// --project-directory) rather than defaulted (e.g. the current directory).
// A remote resource loader (git, oci) must not override an explicit working
// dir with the directory of its downloaded copy.
func (o *Options) SetWorkingDirExplicit(explicit bool) {
o.workingDirExplicit = explicit
}

// serviceRef identifies a reference to a service. It's used to detect cyclic
Expand Down Expand Up @@ -346,7 +361,7 @@ func LoadConfigFiles(ctx context.Context, configFiles []string, workingDir strin
if err != nil {
return nil, err
}
if config.WorkingDir == "" && !isLocalResourceLoader {
if config.WorkingDir == "" && !isLocalResourceLoader && !opts.workingDirExplicit {
config.WorkingDir = filepath.Dir(local)
}
abs, err := filepath.Abs(local)
Expand Down Expand Up @@ -711,7 +726,7 @@ func projectName(details *types.ConfigDetails, opts *Options) error {
details.Environment[consts.ComposeProjectName] = opts.projectName
}()

if opts.projectNameImperativelySet {
if opts.projectNameExplicit {
if NormalizeProjectName(opts.projectName) != opts.projectName {
return InvalidProjectNameErr(opts.projectName)
}
Expand Down
12 changes: 6 additions & 6 deletions loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2785,20 +2785,20 @@ func TestLoadProjectName(t *testing.T) {
wantErr: "project name must not be empty",
},
{
name: "project name from options, not imperatively set; no env",
name: "project name from options, not explicitly set; no env",
options: withProjectName(projectName, false),
},
{
name: "project name from options, imperatively set; no env",
name: "project name from options, explicitly set; no env",
options: withProjectName(projectName, true),
},
{
name: "project name from options, not imperatively set; empty env",
name: "project name from options, not explicitly set; empty env",
env: map[string]string{},
options: withProjectName(projectName, false),
},
{
name: "project name from options, imperatively set; empty env",
name: "project name from options, explicitly set; empty env",
env: map[string]string{},
options: withProjectName(projectName, true),
},
Expand Down Expand Up @@ -2828,9 +2828,9 @@ services:
}
}

func withProjectName(projectName string, imperativelySet bool) func(*Options) {
func withProjectName(projectName string, explicit bool) func(*Options) {
return func(opts *Options) {
opts.SetProjectName(projectName, imperativelySet)
opts.SetProjectName(projectName, explicit)
}
}

Expand Down
Loading