From 2021abcb7dbb6abe34db82956f0ac0afdd2e6d8d Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Fri, 3 Sep 2021 10:08:45 +0200 Subject: [PATCH] introduce WithResolvedPaths to let client code opt for relative or absolute paths Signed-off-by: Nicolas De Loof --- cli/options.go | 10 ++++++ loader/loader.go | 77 +++++++++++++++++++++------------------- loader/loader_test.go | 3 +- loader/normalize.go | 8 +++-- loader/normalize_test.go | 15 ++++---- 5 files changed, 64 insertions(+), 49 deletions(-) diff --git a/cli/options.go b/cli/options.go index 7fde8869..ec45d371 100644 --- a/cli/options.go +++ b/cli/options.go @@ -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"} diff --git a/loader/loader.go b/loader/loader.go index d868c722..935767d3 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -49,6 +49,8 @@ type Options struct { SkipInterpolation bool // Skip normalization SkipNormalization bool + // Resolve paths + ResolvePaths bool // Skip consistency check SkipConsistencyCheck bool // Skip extends @@ -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 } @@ -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 } @@ -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 } @@ -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 @@ -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 @@ -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 @@ -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 } @@ -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 } @@ -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: @@ -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 diff --git a/loader/loader_test.go b/loader/loader_test.go index 2197a1f5..7129c6a8 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -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 }) } @@ -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": { diff --git a/loader/normalize.go b/loader/normalize.go index fd527b7e..30e9d20f 100644 --- a/loader/normalize.go +++ b/loader/normalize.go @@ -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 @@ -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 diff --git a/loader/normalize_test.go b/loader/normalize_test.go index 3143271a..2f500e61 100644 --- a/loader/normalize_test.go +++ b/loader/normalize_test.go @@ -19,7 +19,6 @@ package loader import ( "os" "path/filepath" - "strings" "testing" "github.com/compose-spec/compose-go/types" @@ -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 @@ -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) @@ -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) } @@ -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) }