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: 10 additions & 0 deletions cli/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,16 @@ func WithInterpolation(interpolation bool) ProjectOptionsFn {
}
}

// WithResolvedPaths set ProjectOptions to enable paths resolution
func WithResolvedPaths(resolve bool) ProjectOptionsFn {
return func(o *ProjectOptions) error {
o.loadOptions = append(o.loadOptions, func(options *loader.Options) {
options.ResolvePaths = resolve
})
return nil
}
}

// DefaultFileNames defines the Compose file names for auto-discovery (in order of preference)
var DefaultFileNames = []string{"compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml"}

Expand Down
77 changes: 40 additions & 37 deletions loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type Options struct {
SkipInterpolation bool
// Skip normalization
SkipNormalization bool
// Resolve paths
ResolvePaths bool
// Skip consistency check
SkipConsistencyCheck bool
// Skip extends
Expand Down Expand Up @@ -199,7 +201,7 @@ func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.
}

if !opts.SkipNormalization {
err = normalize(project)
err = normalize(project, opts.ResolvePaths)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -269,11 +271,11 @@ func loadSections(filename string, config map[string]interface{}, configDetails
if err != nil {
return nil, err
}
cfg.Secrets, err = LoadSecrets(getSection(config, "secrets"), configDetails)
cfg.Secrets, err = LoadSecrets(getSection(config, "secrets"), configDetails, opts.ResolvePaths)
if err != nil {
return nil, err
}
cfg.Configs, err = LoadConfigObjs(getSection(config, "configs"), configDetails)
cfg.Configs, err = LoadConfigObjs(getSection(config, "configs"), configDetails, opts.ResolvePaths)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -443,7 +445,7 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter
return nil, err
}

serviceConfig, err := LoadService(name, servicesDict[name].(map[string]interface{}), workingDir, lookupEnv)
serviceConfig, err := LoadService(name, servicesDict[name].(map[string]interface{}), workingDir, lookupEnv, opts.ResolvePaths)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -514,7 +516,7 @@ func loadServiceWithExtends(filename, name string, servicesDict map[string]inter

// LoadService produces a single ServiceConfig from a compose file Dict
// the serviceDict is not validated if directly used. Use Load() to enable validation
func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) {
func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping, resolvePaths bool) (*types.ServiceConfig, error) {
serviceConfig := &types.ServiceConfig{}
if err := Transform(serviceDict, serviceConfig); err != nil {
return nil, err
Expand All @@ -525,8 +527,18 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str
return nil, err
}

if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil {
return nil, err
for i, volume := range serviceConfig.Volumes {
if volume.Type != "bind" {
continue
}

if volume.Source == "" {
return nil, errors.New(`invalid mount config for type "bind": field Source must not be empty`)
}

if resolvePaths {
serviceConfig.Volumes[i] = resolveVolumePath(volume, workingDir, lookupEnv)
}
}

return serviceConfig, nil
Expand Down Expand Up @@ -561,30 +573,19 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l
return nil
}

func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error {
for i, volume := range volumes {
if volume.Type != "bind" {
continue
}

if volume.Source == "" {
return errors.New(`invalid mount config for type "bind": field Source must not be empty`)
}

filePath := expandUser(volume.Source, lookupEnv)
// Check if source is an absolute path (either Unix or Windows), to
// handle a Windows client with a Unix daemon or vice-versa.
//
// Note that this is not required for Docker for Windows when specifying
// a local Windows path, because Docker for Windows translates the Windows
// path into a valid path within the VM.
if !path.IsAbs(filePath) && !isAbs(filePath) {
filePath = absPath(workingDir, filePath)
}
volume.Source = filePath
volumes[i] = volume
}
return nil
func resolveVolumePath(volume types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) types.ServiceVolumeConfig {
filePath := expandUser(volume.Source, lookupEnv)
// Check if source is an absolute path (either Unix or Windows), to
// handle a Windows client with a Unix daemon or vice-versa.
//
// Note that this is not required for Docker for Windows when specifying
// a local Windows path, because Docker for Windows translates the Windows
// path into a valid path within the VM.
if !path.IsAbs(filePath) && !isAbs(filePath) {
filePath = absPath(workingDir, filePath)
}
volume.Source = filePath
return volume
}

// TODO: make this more robust
Expand Down Expand Up @@ -688,13 +689,13 @@ func LoadVolumes(source map[string]interface{}) (map[string]types.VolumeConfig,

// LoadSecrets produces a SecretConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadSecrets(source map[string]interface{}, details types.ConfigDetails) (map[string]types.SecretConfig, error) {
func LoadSecrets(source map[string]interface{}, details types.ConfigDetails, resolvePaths bool) (map[string]types.SecretConfig, error) {
secrets := make(map[string]types.SecretConfig)
if err := Transform(source, &secrets); err != nil {
return secrets, err
}
for name, secret := range secrets {
obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details)
obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details, resolvePaths)
if err != nil {
return nil, err
}
Expand All @@ -706,13 +707,13 @@ func LoadSecrets(source map[string]interface{}, details types.ConfigDetails) (ma

// LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
// the source Dict is not validated if directly used. Use Load() to enable validation
func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails) (map[string]types.ConfigObjConfig, error) {
func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails, resolvePaths bool) (map[string]types.ConfigObjConfig, error) {
configs := make(map[string]types.ConfigObjConfig)
if err := Transform(source, &configs); err != nil {
return configs, err
}
for name, config := range configs {
obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details)
obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details, resolvePaths)
if err != nil {
return nil, err
}
Expand All @@ -722,7 +723,7 @@ func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails)
return configs, nil
}

func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails) (types.FileObjectConfig, error) {
func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails, resolvePaths bool) (types.FileObjectConfig, error) {
// if "external: true"
switch {
case obj.External.External:
Expand All @@ -745,7 +746,9 @@ func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfi
return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name)
}
default:
obj.File = absPath(details.WorkingDir, obj.File)
if resolvePaths {
obj.File = absPath(details.WorkingDir, obj.File)
}
}

return obj, nil
Expand Down
3 changes: 2 additions & 1 deletion loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func loadYAMLWithEnv(yaml string, env map[string]string) (*types.Project, error)
return Load(buildConfigDetails(yaml, env), func(options *Options) {
options.SkipConsistencyCheck = true
options.SkipNormalization = true
options.ResolvePaths = true
})
}

Expand Down Expand Up @@ -1339,7 +1340,7 @@ func TestLoadSecretsWarnOnDeprecatedExternalNameVersion35(t *testing.T) {
},
}
details := types.ConfigDetails{}
secrets, err := LoadSecrets(source, details)
secrets, err := LoadSecrets(source, details, true)
assert.NilError(t, err)
expected := map[string]types.SecretConfig{
"foo": {
Expand Down
8 changes: 5 additions & 3 deletions loader/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
)

// normalize compose project by moving deprecated attributes to their canonical position and injecting implicit defaults
func normalize(project *types.Project) error {
func normalize(project *types.Project, resolvePaths bool) error {
absWorkingDir, err := filepath.Abs(project.WorkingDir)
if err != nil {
return err
Expand Down Expand Up @@ -72,8 +72,10 @@ func normalize(project *types.Project) error {
}
localContext := absPath(project.WorkingDir, s.Build.Context)
if _, err := os.Stat(localContext); err == nil {
s.Build.Context = localContext
s.Build.Dockerfile = absPath(localContext, s.Build.Dockerfile)
if resolvePaths {
s.Build.Context = localContext
s.Build.Dockerfile = absPath(localContext, s.Build.Dockerfile)
}
} else {
// might be a remote http/git context. Unfortunately supported "remote" syntax is highly ambiguous
// in moby/moby and not defined by compose-spec, so let's assume runtime will check
Expand Down
15 changes: 7 additions & 8 deletions loader/normalize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package loader
import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/compose-spec/compose-go/types"
Expand Down Expand Up @@ -59,11 +58,11 @@ func TestNormalizeNetworkNames(t *testing.T) {
},
}

expected := strings.ReplaceAll(strings.ReplaceAll(`services:
expected := `services:
foo:
build:
context: /some/path/testdata
dockerfile: /some/path/testdata/Dockerfile
context: ./testdata
dockerfile: Dockerfile
args:
FOO: BAR
ZOT: null
Expand All @@ -79,8 +78,8 @@ networks:
name: CustomName
mynet:
name: myProject_mynet
`, "/some/path", wd), "/", string(filepath.Separator))
err := normalize(&project)
`
err := normalize(&project, false)
assert.NilError(t, err)
marshal, err := yaml.Marshal(project)
assert.NilError(t, err)
Expand All @@ -104,7 +103,7 @@ func TestNormalizeAbsolutePaths(t *testing.T) {
WorkingDir: absWorkingDir,
ComposeFiles: []string{absComposeFile, absOverrideFile},
}
err := normalize(&project)
err := normalize(&project, false)
assert.NilError(t, err)
assert.DeepEqual(t, expected, project)
}
Expand Down Expand Up @@ -142,7 +141,7 @@ func TestNormalizeVolumes(t *testing.T) {
WorkingDir: absCwd,
ComposeFiles: []string{},
}
err := normalize(&project)
err := normalize(&project, false)
assert.NilError(t, err)
assert.DeepEqual(t, expected, project)
}