Skip to content

Commit 1700392

Browse files
committed
Leverage crosspath lib for home / working dir expansion, and detection of absolute path in cross-plat
Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
1 parent 3519d9a commit 1700392

6 files changed

Lines changed: 126 additions & 28 deletions

File tree

cli/command/stack/loader/loader.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/docker/cli/cli/compose/loader"
1515
"github.com/docker/cli/cli/compose/schema"
1616
composetypes "github.com/docker/cli/cli/compose/types"
17+
"github.com/docker/docker/pkg/homedir"
1718
"github.com/pkg/errors"
1819
)
1920

@@ -97,6 +98,7 @@ func getConfigDetails(composefiles []string, stdin io.Reader) (composetypes.Conf
9798
// Take the first file version (2 files can't have different version)
9899
details.Version = schema.Version(details.ConfigFiles[0].Config)
99100
details.Environment, err = buildEnvironment(os.Environ())
101+
details.HomeDir = homedir.Get()
100102
return details, err
101103
}
102104

cli/command/stack/loader/loader_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77
"testing"
88

9+
"github.com/docker/docker/pkg/homedir"
910
"gotest.tools/assert"
1011
is "gotest.tools/assert/cmp"
1112
"gotest.tools/fs"
@@ -24,6 +25,7 @@ services:
2425
details, err := getConfigDetails([]string{file.Path()}, nil)
2526
assert.NilError(t, err)
2627
assert.Check(t, is.Equal(filepath.Dir(file.Path()), details.WorkingDir))
28+
assert.Check(t, is.Equal(homedir.Get(), details.HomeDir))
2729
assert.Assert(t, is.Len(details.ConfigFiles, 1))
2830
assert.Check(t, is.Equal("3.0", details.ConfigFiles[0].Config["version"]))
2931
assert.Check(t, is.Len(details.Environment, len(os.Environ())))
@@ -41,6 +43,7 @@ services:
4143
cwd, err := os.Getwd()
4244
assert.NilError(t, err)
4345
assert.Check(t, is.Equal(cwd, details.WorkingDir))
46+
assert.Check(t, is.Equal(homedir.Get(), details.HomeDir))
4447
assert.Assert(t, is.Len(details.ConfigFiles, 1))
4548
assert.Check(t, is.Equal("3.0", details.ConfigFiles[0].Config["version"]))
4649
assert.Check(t, is.Len(details.Environment, len(os.Environ())))

cli/compose/loader/full-struct_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"path/filepath"
66
"time"
77

8+
"github.com/simonferquel/crosspath"
9+
810
"github.com/docker/cli/cli/compose/types"
911
)
1012

@@ -20,6 +22,7 @@ func fullExampleConfig(workingDir, homeDir string) *types.Config {
2022
}
2123

2224
func services(workingDir, homeDir string) []types.ServiceConfig {
25+
configsPath, _ := crosspath.ParsePathWithDefaults(homeDir + "/configs")
2326
return []types.ServiceConfig{
2427
{
2528
Name: "foo",
@@ -350,7 +353,7 @@ func services(workingDir, homeDir string) []types.ServiceConfig {
350353
{Source: "/opt/data", Target: "/var/lib/mysql", Type: "bind"},
351354
{Source: workingDir, Target: "/code", Type: "bind"},
352355
{Source: filepath.Join(workingDir, "static"), Target: "/var/www/html", Type: "bind"},
353-
{Source: homeDir + "/configs", Target: "/etc/configs/", Type: "bind", ReadOnly: true},
356+
{Source: configsPath.String(), Target: "/etc/configs/", Type: "bind", ReadOnly: true},
354357
{Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"},
355358
{Source: filepath.Join(workingDir, "opt"), Target: "/opt", Consistency: "cached", Type: "bind"},
356359
{Target: "/opt", Type: "tmpfs", Tmpfs: &types.ServiceVolumeTmpfs{

cli/compose/loader/loader.go

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package loader
22

33
import (
44
"fmt"
5-
"path"
65
"path/filepath"
76
"reflect"
87
"sort"
@@ -18,6 +17,7 @@ import (
1817
shellwords "github.com/mattn/go-shellwords"
1918
"github.com/mitchellh/mapstructure"
2019
"github.com/pkg/errors"
20+
"github.com/simonferquel/crosspath"
2121
"github.com/sirupsen/logrus"
2222
yaml "gopkg.in/yaml.v2"
2323
)
@@ -109,7 +109,7 @@ func loadSections(config map[string]interface{}, configDetails types.ConfigDetai
109109
{
110110
key: "services",
111111
fnc: func(config map[string]interface{}) error {
112-
cfg.Services, err = LoadServices(config, configDetails.WorkingDir, configDetails.LookupEnv)
112+
cfg.Services, err = LoadServices(config, configDetails.WorkingDir, configDetails.HomeDir, configDetails.LookupEnv)
113113
return err
114114
},
115115
},
@@ -334,11 +334,11 @@ func formatInvalidKeyError(keyPrefix string, key interface{}) error {
334334

335335
// LoadServices produces a ServiceConfig map from a compose file Dict
336336
// the servicesDict is not validated if directly used. Use Load() to enable validation
337-
func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) {
337+
func LoadServices(servicesDict map[string]interface{}, workingDir, homeDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) {
338338
var services []types.ServiceConfig
339339

340340
for name, serviceDef := range servicesDict {
341-
serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, lookupEnv)
341+
serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, homeDir, lookupEnv)
342342
if err != nil {
343343
return nil, err
344344
}
@@ -350,7 +350,7 @@ func LoadServices(servicesDict map[string]interface{}, workingDir string, lookup
350350

351351
// LoadService produces a single ServiceConfig from a compose file Dict
352352
// the serviceDict is not validated if directly used. Use Load() to enable validation
353-
func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) {
353+
func LoadService(name string, serviceDict map[string]interface{}, workingDir, homeDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) {
354354
serviceConfig := &types.ServiceConfig{}
355355
if err := transform(serviceDict, serviceConfig); err != nil {
356356
return nil, err
@@ -361,7 +361,7 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str
361361
return nil, err
362362
}
363363

364-
if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil {
364+
if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, homeDir); err != nil {
365365
return nil, err
366366
}
367367
return serviceConfig, nil
@@ -402,7 +402,7 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l
402402
return nil
403403
}
404404

405-
func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error {
405+
func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir, homeDir string) error {
406406
for i, volume := range volumes {
407407
if volume.Type != "bind" {
408408
continue
@@ -412,32 +412,43 @@ func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string,
412412
return errors.New(`invalid mount config for type "bind": field Source must not be empty`)
413413
}
414414

415-
filePath := expandUser(volume.Source, lookupEnv)
416-
// Check for a Unix absolute path first, to handle a Windows client
417-
// with a Unix daemon. This handles a Windows client connecting to a
418-
// Unix daemon. Note that this is not required for Docker for Windows
419-
// when specifying a local Windows path, because Docker for Windows
420-
// translates the Windows path into a valid path within the VM.
421-
if !path.IsAbs(filePath) {
422-
filePath = absPath(workingDir, filePath)
415+
filePath, err := transformFilePath(volume.Source, workingDir, homeDir)
416+
if err != nil {
417+
return err
423418
}
419+
424420
volume.Source = filePath
425421
volumes[i] = volume
426422
}
427423
return nil
428424
}
429425

430-
// TODO: make this more robust
431-
func expandUser(path string, lookupEnv template.Mapping) string {
432-
if strings.HasPrefix(path, "~") {
433-
home, ok := lookupEnv("HOME")
434-
if !ok {
435-
logrus.Warn("cannot expand '~', because the environment lacks HOME")
436-
return path
426+
func transformFilePath(origin string, workingDir, homeDir string) (string, error) {
427+
path, err := crosspath.ParsePathWithDefaults(origin)
428+
if err != nil {
429+
return "", err
430+
}
431+
switch path.Kind() {
432+
case crosspath.Relative:
433+
basePath, err := crosspath.ParsePathWithDefaults(workingDir)
434+
if err != nil {
435+
return "", err
436+
}
437+
path, err = basePath.Join(path)
438+
if err != nil {
439+
return "", err
440+
}
441+
case crosspath.HomeRooted:
442+
basePath, err := crosspath.ParsePathWithDefaults(homeDir)
443+
if err != nil {
444+
return "", err
445+
}
446+
path, err = basePath.Join(path)
447+
if err != nil {
448+
return "", err
437449
}
438-
return strings.Replace(path, "~", home, 1)
439450
}
440-
return path
451+
return path.String(), nil
441452
}
442453

443454
func transformUlimits(data interface{}) (interface{}, error) {

cli/compose/loader/loader_test.go

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/docker/cli/cli/compose/types"
13+
"github.com/docker/docker/pkg/homedir"
1314
"github.com/google/go-cmp/cmp/cmpopts"
1415
"github.com/sirupsen/logrus"
1516
"gotest.tools/assert"
@@ -28,6 +29,7 @@ func buildConfigDetails(source map[string]interface{}, env map[string]string) ty
2829
{Filename: "filename.yml", Config: source},
2930
},
3031
Environment: env,
32+
HomeDir: homedir.Get(),
3133
}
3234
}
3335

@@ -812,15 +814,14 @@ func TestFullExample(t *testing.T) {
812814
bytes, err := ioutil.ReadFile("full-example.yml")
813815
assert.NilError(t, err)
814816

815-
homeDir := "/home/foo"
816-
env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"}
817+
env := map[string]string{"QUX": "qux_from_environment"}
817818
config, err := loadYAMLWithEnv(string(bytes), env)
818819
assert.NilError(t, err)
819820

820821
workingDir, err := os.Getwd()
821822
assert.NilError(t, err)
822823

823-
expectedConfig := fullExampleConfig(workingDir, homeDir)
824+
expectedConfig := fullExampleConfig(workingDir, homedir.Get())
824825

825826
assert.Check(t, is.DeepEqual(expectedConfig.Services, config.Services))
826827
assert.Check(t, is.DeepEqual(expectedConfig.Networks, config.Networks))
@@ -1285,6 +1286,7 @@ func TestLoadSecretsWarnOnDeprecatedExternalNameVersion35(t *testing.T) {
12851286
}
12861287
details := types.ConfigDetails{
12871288
Version: "3.5",
1289+
HomeDir: homedir.Get(),
12881290
}
12891291
secrets, err := LoadSecrets(source, details)
12901292
assert.NilError(t, err)
@@ -1395,3 +1397,79 @@ networks:
13951397
}
13961398
assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty())
13971399
}
1400+
1401+
func TestFilePathsCrossPlat(t *testing.T) {
1402+
cases := []struct {
1403+
path string
1404+
workingDir string
1405+
expected string
1406+
homeDir string
1407+
}{
1408+
{
1409+
path: `/var/data`,
1410+
workingDir: `/working/dir`,
1411+
expected: `/var/data`,
1412+
},
1413+
{
1414+
path: `/var/data`,
1415+
workingDir: `c:\working\dir`,
1416+
expected: `/var/data`,
1417+
},
1418+
{
1419+
path: `relative/data`,
1420+
workingDir: `/working/dir`,
1421+
expected: `/working/dir/relative/data`,
1422+
},
1423+
{
1424+
path: `relative/data`,
1425+
workingDir: `c:\working\dir`,
1426+
expected: `c:\working\dir\relative\data`,
1427+
},
1428+
{
1429+
path: `c:\var\data`,
1430+
workingDir: `/working/dir`,
1431+
expected: `c:\var\data`,
1432+
},
1433+
{
1434+
path: `c:\var\data`,
1435+
workingDir: `c:\working\dir`,
1436+
expected: `c:\var\data`,
1437+
},
1438+
{
1439+
path: `relative\data`,
1440+
workingDir: `/working/dir`,
1441+
expected: `/working/dir/relative/data`,
1442+
},
1443+
{
1444+
path: `relative\data`,
1445+
workingDir: `c:\working\dir`,
1446+
expected: `c:\working\dir\relative\data`,
1447+
},
1448+
{
1449+
path: `~\homerooted\data`,
1450+
homeDir: `c:\users\user`,
1451+
expected: `c:\users\user\homerooted\data`,
1452+
},
1453+
{
1454+
path: `~\homerooted\data`,
1455+
homeDir: `/home/user`,
1456+
expected: `/home/user/homerooted/data`,
1457+
},
1458+
1459+
{
1460+
path: `~/homerooted/data`,
1461+
homeDir: `c:\users\user`,
1462+
expected: `c:\users\user\homerooted\data`,
1463+
},
1464+
{
1465+
path: `~/homerooted/data`,
1466+
homeDir: `/home/user`,
1467+
expected: `/home/user/homerooted/data`,
1468+
},
1469+
}
1470+
for _, c := range cases {
1471+
result, err := transformFilePath(c.path, c.workingDir, c.homeDir)
1472+
assert.NilError(t, err)
1473+
assert.Equal(t, c.expected, result)
1474+
}
1475+
}

cli/compose/types/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type ConfigFile struct {
5858
type ConfigDetails struct {
5959
Version string
6060
WorkingDir string
61+
HomeDir string
6162
ConfigFiles []ConfigFile
6263
Environment map[string]string
6364
}

0 commit comments

Comments
 (0)