github.com/itscaro/cli@v0.0.0-20190705081621-c9db0fe93829/cli/compose/loader/loader.go (about)

     1  package loader
     2  
     3  import (
     4  	"fmt"
     5  	"path"
     6  	"path/filepath"
     7  	"reflect"
     8  	"sort"
     9  	"strings"
    10  	"time"
    11  
    12  	interp "github.com/docker/cli/cli/compose/interpolation"
    13  	"github.com/docker/cli/cli/compose/schema"
    14  	"github.com/docker/cli/cli/compose/template"
    15  	"github.com/docker/cli/cli/compose/types"
    16  	"github.com/docker/cli/opts"
    17  	"github.com/docker/docker/api/types/versions"
    18  	"github.com/docker/go-connections/nat"
    19  	units "github.com/docker/go-units"
    20  	shellwords "github.com/mattn/go-shellwords"
    21  	"github.com/mitchellh/mapstructure"
    22  	"github.com/pkg/errors"
    23  	"github.com/sirupsen/logrus"
    24  	yaml "gopkg.in/yaml.v2"
    25  )
    26  
    27  // Options supported by Load
    28  type Options struct {
    29  	// Skip schema validation
    30  	SkipValidation bool
    31  	// Skip interpolation
    32  	SkipInterpolation bool
    33  	// Interpolation options
    34  	Interpolate *interp.Options
    35  }
    36  
    37  // ParseYAML reads the bytes from a file, parses the bytes into a mapping
    38  // structure, and returns it.
    39  func ParseYAML(source []byte) (map[string]interface{}, error) {
    40  	var cfg interface{}
    41  	if err := yaml.Unmarshal(source, &cfg); err != nil {
    42  		return nil, err
    43  	}
    44  	cfgMap, ok := cfg.(map[interface{}]interface{})
    45  	if !ok {
    46  		return nil, errors.Errorf("Top-level object must be a mapping")
    47  	}
    48  	converted, err := convertToStringKeysRecursive(cfgMap, "")
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	return converted.(map[string]interface{}), nil
    53  }
    54  
    55  // Load reads a ConfigDetails and returns a fully loaded configuration
    56  func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.Config, error) {
    57  	if len(configDetails.ConfigFiles) < 1 {
    58  		return nil, errors.Errorf("No files specified")
    59  	}
    60  
    61  	opts := &Options{
    62  		Interpolate: &interp.Options{
    63  			Substitute:      template.Substitute,
    64  			LookupValue:     configDetails.LookupEnv,
    65  			TypeCastMapping: interpolateTypeCastMapping,
    66  		},
    67  	}
    68  
    69  	for _, op := range options {
    70  		op(opts)
    71  	}
    72  
    73  	configs := []*types.Config{}
    74  	var err error
    75  
    76  	for _, file := range configDetails.ConfigFiles {
    77  		configDict := file.Config
    78  		version := schema.Version(configDict)
    79  		if configDetails.Version == "" {
    80  			configDetails.Version = version
    81  		}
    82  		if configDetails.Version != version {
    83  			return nil, errors.Errorf("version mismatched between two composefiles : %v and %v", configDetails.Version, version)
    84  		}
    85  
    86  		if err := validateForbidden(configDict); err != nil {
    87  			return nil, err
    88  		}
    89  
    90  		if !opts.SkipInterpolation {
    91  			configDict, err = interpolateConfig(configDict, *opts.Interpolate)
    92  			if err != nil {
    93  				return nil, err
    94  			}
    95  		}
    96  
    97  		if !opts.SkipValidation {
    98  			if err := schema.Validate(configDict, configDetails.Version); err != nil {
    99  				return nil, err
   100  			}
   101  		}
   102  
   103  		cfg, err := loadSections(configDict, configDetails)
   104  		if err != nil {
   105  			return nil, err
   106  		}
   107  		cfg.Filename = file.Filename
   108  
   109  		configs = append(configs, cfg)
   110  	}
   111  
   112  	return merge(configs)
   113  }
   114  
   115  func validateForbidden(configDict map[string]interface{}) error {
   116  	servicesDict, ok := configDict["services"].(map[string]interface{})
   117  	if !ok {
   118  		return nil
   119  	}
   120  	forbidden := getProperties(servicesDict, types.ForbiddenProperties)
   121  	if len(forbidden) > 0 {
   122  		return &ForbiddenPropertiesError{Properties: forbidden}
   123  	}
   124  	return nil
   125  }
   126  
   127  func loadSections(config map[string]interface{}, configDetails types.ConfigDetails) (*types.Config, error) {
   128  	var err error
   129  	cfg := types.Config{
   130  		Version: schema.Version(config),
   131  	}
   132  
   133  	var loaders = []struct {
   134  		key string
   135  		fnc func(config map[string]interface{}) error
   136  	}{
   137  		{
   138  			key: "services",
   139  			fnc: func(config map[string]interface{}) error {
   140  				cfg.Services, err = LoadServices(config, configDetails.WorkingDir, configDetails.LookupEnv)
   141  				return err
   142  			},
   143  		},
   144  		{
   145  			key: "networks",
   146  			fnc: func(config map[string]interface{}) error {
   147  				cfg.Networks, err = LoadNetworks(config, configDetails.Version)
   148  				return err
   149  			},
   150  		},
   151  		{
   152  			key: "volumes",
   153  			fnc: func(config map[string]interface{}) error {
   154  				cfg.Volumes, err = LoadVolumes(config, configDetails.Version)
   155  				return err
   156  			},
   157  		},
   158  		{
   159  			key: "secrets",
   160  			fnc: func(config map[string]interface{}) error {
   161  				cfg.Secrets, err = LoadSecrets(config, configDetails)
   162  				return err
   163  			},
   164  		},
   165  		{
   166  			key: "configs",
   167  			fnc: func(config map[string]interface{}) error {
   168  				cfg.Configs, err = LoadConfigObjs(config, configDetails)
   169  				return err
   170  			},
   171  		},
   172  	}
   173  	for _, loader := range loaders {
   174  		if err := loader.fnc(getSection(config, loader.key)); err != nil {
   175  			return nil, err
   176  		}
   177  	}
   178  	cfg.Extras = getExtras(config)
   179  	return &cfg, nil
   180  }
   181  
   182  func getSection(config map[string]interface{}, key string) map[string]interface{} {
   183  	section, ok := config[key]
   184  	if !ok {
   185  		return make(map[string]interface{})
   186  	}
   187  	return section.(map[string]interface{})
   188  }
   189  
   190  // GetUnsupportedProperties returns the list of any unsupported properties that are
   191  // used in the Compose files.
   192  func GetUnsupportedProperties(configDicts ...map[string]interface{}) []string {
   193  	unsupported := map[string]bool{}
   194  
   195  	for _, configDict := range configDicts {
   196  		for _, service := range getServices(configDict) {
   197  			serviceDict := service.(map[string]interface{})
   198  			for _, property := range types.UnsupportedProperties {
   199  				if _, isSet := serviceDict[property]; isSet {
   200  					unsupported[property] = true
   201  				}
   202  			}
   203  		}
   204  	}
   205  
   206  	return sortedKeys(unsupported)
   207  }
   208  
   209  func sortedKeys(set map[string]bool) []string {
   210  	var keys []string
   211  	for key := range set {
   212  		keys = append(keys, key)
   213  	}
   214  	sort.Strings(keys)
   215  	return keys
   216  }
   217  
   218  // GetDeprecatedProperties returns the list of any deprecated properties that
   219  // are used in the compose files.
   220  func GetDeprecatedProperties(configDicts ...map[string]interface{}) map[string]string {
   221  	deprecated := map[string]string{}
   222  
   223  	for _, configDict := range configDicts {
   224  		deprecatedProperties := getProperties(getServices(configDict), types.DeprecatedProperties)
   225  		for key, value := range deprecatedProperties {
   226  			deprecated[key] = value
   227  		}
   228  	}
   229  
   230  	return deprecated
   231  }
   232  
   233  func getProperties(services map[string]interface{}, propertyMap map[string]string) map[string]string {
   234  	output := map[string]string{}
   235  
   236  	for _, service := range services {
   237  		if serviceDict, ok := service.(map[string]interface{}); ok {
   238  			for property, description := range propertyMap {
   239  				if _, isSet := serviceDict[property]; isSet {
   240  					output[property] = description
   241  				}
   242  			}
   243  		}
   244  	}
   245  
   246  	return output
   247  }
   248  
   249  // ForbiddenPropertiesError is returned when there are properties in the Compose
   250  // file that are forbidden.
   251  type ForbiddenPropertiesError struct {
   252  	Properties map[string]string
   253  }
   254  
   255  func (e *ForbiddenPropertiesError) Error() string {
   256  	return "Configuration contains forbidden properties"
   257  }
   258  
   259  func getServices(configDict map[string]interface{}) map[string]interface{} {
   260  	if services, ok := configDict["services"]; ok {
   261  		if servicesDict, ok := services.(map[string]interface{}); ok {
   262  			return servicesDict
   263  		}
   264  	}
   265  
   266  	return map[string]interface{}{}
   267  }
   268  
   269  // Transform converts the source into the target struct with compose types transformer
   270  // and the specified transformers if any.
   271  func Transform(source interface{}, target interface{}, additionalTransformers ...Transformer) error {
   272  	data := mapstructure.Metadata{}
   273  	config := &mapstructure.DecoderConfig{
   274  		DecodeHook: mapstructure.ComposeDecodeHookFunc(
   275  			createTransformHook(additionalTransformers...),
   276  			mapstructure.StringToTimeDurationHookFunc()),
   277  		Result:   target,
   278  		Metadata: &data,
   279  	}
   280  	decoder, err := mapstructure.NewDecoder(config)
   281  	if err != nil {
   282  		return err
   283  	}
   284  	return decoder.Decode(source)
   285  }
   286  
   287  // Transformer defines a map to type transformer
   288  type Transformer struct {
   289  	TypeOf reflect.Type
   290  	Func   func(interface{}) (interface{}, error)
   291  }
   292  
   293  func createTransformHook(additionalTransformers ...Transformer) mapstructure.DecodeHookFuncType {
   294  	transforms := map[reflect.Type]func(interface{}) (interface{}, error){
   295  		reflect.TypeOf(types.External{}):                         transformExternal,
   296  		reflect.TypeOf(types.HealthCheckTest{}):                  transformHealthCheckTest,
   297  		reflect.TypeOf(types.ShellCommand{}):                     transformShellCommand,
   298  		reflect.TypeOf(types.StringList{}):                       transformStringList,
   299  		reflect.TypeOf(map[string]string{}):                      transformMapStringString,
   300  		reflect.TypeOf(types.UlimitsConfig{}):                    transformUlimits,
   301  		reflect.TypeOf(types.UnitBytes(0)):                       transformSize,
   302  		reflect.TypeOf([]types.ServicePortConfig{}):              transformServicePort,
   303  		reflect.TypeOf(types.ServiceSecretConfig{}):              transformStringSourceMap,
   304  		reflect.TypeOf(types.ServiceConfigObjConfig{}):           transformStringSourceMap,
   305  		reflect.TypeOf(types.StringOrNumberList{}):               transformStringOrNumberList,
   306  		reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): transformServiceNetworkMap,
   307  		reflect.TypeOf(types.Mapping{}):                          transformMappingOrListFunc("=", false),
   308  		reflect.TypeOf(types.MappingWithEquals{}):                transformMappingOrListFunc("=", true),
   309  		reflect.TypeOf(types.Labels{}):                           transformMappingOrListFunc("=", false),
   310  		reflect.TypeOf(types.MappingWithColon{}):                 transformMappingOrListFunc(":", false),
   311  		reflect.TypeOf(types.HostsList{}):                        transformListOrMappingFunc(":", false),
   312  		reflect.TypeOf(types.ServiceVolumeConfig{}):              transformServiceVolumeConfig,
   313  		reflect.TypeOf(types.BuildConfig{}):                      transformBuildConfig,
   314  		reflect.TypeOf(types.Duration(0)):                        transformStringToDuration,
   315  	}
   316  
   317  	for _, transformer := range additionalTransformers {
   318  		transforms[transformer.TypeOf] = transformer.Func
   319  	}
   320  
   321  	return func(_ reflect.Type, target reflect.Type, data interface{}) (interface{}, error) {
   322  		transform, ok := transforms[target]
   323  		if !ok {
   324  			return data, nil
   325  		}
   326  		return transform(data)
   327  	}
   328  }
   329  
   330  // keys needs to be converted to strings for jsonschema
   331  func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) {
   332  	if mapping, ok := value.(map[interface{}]interface{}); ok {
   333  		dict := make(map[string]interface{})
   334  		for key, entry := range mapping {
   335  			str, ok := key.(string)
   336  			if !ok {
   337  				return nil, formatInvalidKeyError(keyPrefix, key)
   338  			}
   339  			var newKeyPrefix string
   340  			if keyPrefix == "" {
   341  				newKeyPrefix = str
   342  			} else {
   343  				newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str)
   344  			}
   345  			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
   346  			if err != nil {
   347  				return nil, err
   348  			}
   349  			dict[str] = convertedEntry
   350  		}
   351  		return dict, nil
   352  	}
   353  	if list, ok := value.([]interface{}); ok {
   354  		var convertedList []interface{}
   355  		for index, entry := range list {
   356  			newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index)
   357  			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
   358  			if err != nil {
   359  				return nil, err
   360  			}
   361  			convertedList = append(convertedList, convertedEntry)
   362  		}
   363  		return convertedList, nil
   364  	}
   365  	return value, nil
   366  }
   367  
   368  func formatInvalidKeyError(keyPrefix string, key interface{}) error {
   369  	var location string
   370  	if keyPrefix == "" {
   371  		location = "at top level"
   372  	} else {
   373  		location = fmt.Sprintf("in %s", keyPrefix)
   374  	}
   375  	return errors.Errorf("Non-string key %s: %#v", location, key)
   376  }
   377  
   378  // LoadServices produces a ServiceConfig map from a compose file Dict
   379  // the servicesDict is not validated if directly used. Use Load() to enable validation
   380  func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) {
   381  	var services []types.ServiceConfig
   382  
   383  	for name, serviceDef := range servicesDict {
   384  		serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, lookupEnv)
   385  		if err != nil {
   386  			return nil, err
   387  		}
   388  		services = append(services, *serviceConfig)
   389  	}
   390  
   391  	return services, nil
   392  }
   393  
   394  // LoadService produces a single ServiceConfig from a compose file Dict
   395  // the serviceDict is not validated if directly used. Use Load() to enable validation
   396  func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) {
   397  	serviceConfig := &types.ServiceConfig{}
   398  	if err := Transform(serviceDict, serviceConfig); err != nil {
   399  		return nil, err
   400  	}
   401  	serviceConfig.Name = name
   402  
   403  	if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil {
   404  		return nil, err
   405  	}
   406  
   407  	if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil {
   408  		return nil, err
   409  	}
   410  
   411  	serviceConfig.Extras = getExtras(serviceDict)
   412  
   413  	return serviceConfig, nil
   414  }
   415  
   416  func loadExtras(name string, source map[string]interface{}) map[string]interface{} {
   417  	if dict, ok := source[name].(map[string]interface{}); ok {
   418  		return getExtras(dict)
   419  	}
   420  	return nil
   421  }
   422  
   423  func getExtras(dict map[string]interface{}) map[string]interface{} {
   424  	extras := map[string]interface{}{}
   425  	for key, value := range dict {
   426  		if strings.HasPrefix(key, "x-") {
   427  			extras[key] = value
   428  		}
   429  	}
   430  	if len(extras) == 0 {
   431  		return nil
   432  	}
   433  	return extras
   434  }
   435  
   436  func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv template.Mapping) {
   437  	for k, v := range vars {
   438  		interpolatedV, ok := lookupEnv(k)
   439  		if (v == nil || *v == "") && ok {
   440  			// lookupEnv is prioritized over vars
   441  			environment[k] = &interpolatedV
   442  		} else {
   443  			environment[k] = v
   444  		}
   445  	}
   446  }
   447  
   448  func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error {
   449  	environment := make(map[string]*string)
   450  
   451  	if len(serviceConfig.EnvFile) > 0 {
   452  		var envVars []string
   453  
   454  		for _, file := range serviceConfig.EnvFile {
   455  			filePath := absPath(workingDir, file)
   456  			fileVars, err := opts.ParseEnvFile(filePath)
   457  			if err != nil {
   458  				return err
   459  			}
   460  			envVars = append(envVars, fileVars...)
   461  		}
   462  		updateEnvironment(environment,
   463  			opts.ConvertKVStringsToMapWithNil(envVars), lookupEnv)
   464  	}
   465  
   466  	updateEnvironment(environment, serviceConfig.Environment, lookupEnv)
   467  	serviceConfig.Environment = environment
   468  	return nil
   469  }
   470  
   471  func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error {
   472  	for i, volume := range volumes {
   473  		if volume.Type != "bind" {
   474  			continue
   475  		}
   476  
   477  		if volume.Source == "" {
   478  			return errors.New(`invalid mount config for type "bind": field Source must not be empty`)
   479  		}
   480  
   481  		filePath := expandUser(volume.Source, lookupEnv)
   482  		// Check for a Unix absolute path first, to handle a Windows client
   483  		// with a Unix daemon. This handles a Windows client connecting to a
   484  		// Unix daemon. Note that this is not required for Docker for Windows
   485  		// when specifying a local Windows path, because Docker for Windows
   486  		// translates the Windows path into a valid path within the VM.
   487  		if !path.IsAbs(filePath) {
   488  			filePath = absPath(workingDir, filePath)
   489  		}
   490  		volume.Source = filePath
   491  		volumes[i] = volume
   492  	}
   493  	return nil
   494  }
   495  
   496  // TODO: make this more robust
   497  func expandUser(path string, lookupEnv template.Mapping) string {
   498  	if strings.HasPrefix(path, "~") {
   499  		home, ok := lookupEnv("HOME")
   500  		if !ok {
   501  			logrus.Warn("cannot expand '~', because the environment lacks HOME")
   502  			return path
   503  		}
   504  		return strings.Replace(path, "~", home, 1)
   505  	}
   506  	return path
   507  }
   508  
   509  func transformUlimits(data interface{}) (interface{}, error) {
   510  	switch value := data.(type) {
   511  	case int:
   512  		return types.UlimitsConfig{Single: value}, nil
   513  	case map[string]interface{}:
   514  		ulimit := types.UlimitsConfig{}
   515  		ulimit.Soft = value["soft"].(int)
   516  		ulimit.Hard = value["hard"].(int)
   517  		return ulimit, nil
   518  	default:
   519  		return data, errors.Errorf("invalid type %T for ulimits", value)
   520  	}
   521  }
   522  
   523  // LoadNetworks produces a NetworkConfig map from a compose file Dict
   524  // the source Dict is not validated if directly used. Use Load() to enable validation
   525  func LoadNetworks(source map[string]interface{}, version string) (map[string]types.NetworkConfig, error) {
   526  	networks := make(map[string]types.NetworkConfig)
   527  	err := Transform(source, &networks)
   528  	if err != nil {
   529  		return networks, err
   530  	}
   531  	for name, network := range networks {
   532  		if !network.External.External {
   533  			continue
   534  		}
   535  		switch {
   536  		case network.External.Name != "":
   537  			if network.Name != "" {
   538  				return nil, errors.Errorf("network %s: network.external.name and network.name conflict; only use network.name", name)
   539  			}
   540  			if versions.GreaterThanOrEqualTo(version, "3.5") {
   541  				logrus.Warnf("network %s: network.external.name is deprecated in favor of network.name", name)
   542  			}
   543  			network.Name = network.External.Name
   544  			network.External.Name = ""
   545  		case network.Name == "":
   546  			network.Name = name
   547  		}
   548  		network.Extras = loadExtras(name, source)
   549  		networks[name] = network
   550  	}
   551  	return networks, nil
   552  }
   553  
   554  func externalVolumeError(volume, key string) error {
   555  	return errors.Errorf(
   556  		"conflicting parameters \"external\" and %q specified for volume %q",
   557  		key, volume)
   558  }
   559  
   560  // LoadVolumes produces a VolumeConfig map from a compose file Dict
   561  // the source Dict is not validated if directly used. Use Load() to enable validation
   562  func LoadVolumes(source map[string]interface{}, version string) (map[string]types.VolumeConfig, error) {
   563  	volumes := make(map[string]types.VolumeConfig)
   564  	if err := Transform(source, &volumes); err != nil {
   565  		return volumes, err
   566  	}
   567  
   568  	for name, volume := range volumes {
   569  		if !volume.External.External {
   570  			continue
   571  		}
   572  		switch {
   573  		case volume.Driver != "":
   574  			return nil, externalVolumeError(name, "driver")
   575  		case len(volume.DriverOpts) > 0:
   576  			return nil, externalVolumeError(name, "driver_opts")
   577  		case len(volume.Labels) > 0:
   578  			return nil, externalVolumeError(name, "labels")
   579  		case volume.External.Name != "":
   580  			if volume.Name != "" {
   581  				return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)
   582  			}
   583  			if versions.GreaterThanOrEqualTo(version, "3.4") {
   584  				logrus.Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name)
   585  			}
   586  			volume.Name = volume.External.Name
   587  			volume.External.Name = ""
   588  		case volume.Name == "":
   589  			volume.Name = name
   590  		}
   591  		volume.Extras = loadExtras(name, source)
   592  		volumes[name] = volume
   593  	}
   594  	return volumes, nil
   595  }
   596  
   597  // LoadSecrets produces a SecretConfig map from a compose file Dict
   598  // the source Dict is not validated if directly used. Use Load() to enable validation
   599  func LoadSecrets(source map[string]interface{}, details types.ConfigDetails) (map[string]types.SecretConfig, error) {
   600  	secrets := make(map[string]types.SecretConfig)
   601  	if err := Transform(source, &secrets); err != nil {
   602  		return secrets, err
   603  	}
   604  	for name, secret := range secrets {
   605  		obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details)
   606  		if err != nil {
   607  			return nil, err
   608  		}
   609  		secretConfig := types.SecretConfig(obj)
   610  		secretConfig.Extras = loadExtras(name, source)
   611  		secrets[name] = secretConfig
   612  	}
   613  	return secrets, nil
   614  }
   615  
   616  // LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
   617  // the source Dict is not validated if directly used. Use Load() to enable validation
   618  func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails) (map[string]types.ConfigObjConfig, error) {
   619  	configs := make(map[string]types.ConfigObjConfig)
   620  	if err := Transform(source, &configs); err != nil {
   621  		return configs, err
   622  	}
   623  	for name, config := range configs {
   624  		obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details)
   625  		if err != nil {
   626  			return nil, err
   627  		}
   628  		configConfig := types.ConfigObjConfig(obj)
   629  		configConfig.Extras = loadExtras(name, source)
   630  		configs[name] = configConfig
   631  	}
   632  	return configs, nil
   633  }
   634  
   635  func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails) (types.FileObjectConfig, error) {
   636  	// if "external: true"
   637  	switch {
   638  	case obj.External.External:
   639  		// handle deprecated external.name
   640  		if obj.External.Name != "" {
   641  			if obj.Name != "" {
   642  				return obj, errors.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name)
   643  			}
   644  			if versions.GreaterThanOrEqualTo(details.Version, "3.5") {
   645  				logrus.Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name)
   646  			}
   647  			obj.Name = obj.External.Name
   648  			obj.External.Name = ""
   649  		} else {
   650  			if obj.Name == "" {
   651  				obj.Name = name
   652  			}
   653  		}
   654  		// if not "external: true"
   655  	case obj.Driver != "":
   656  		if obj.File != "" {
   657  			return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name)
   658  		}
   659  	default:
   660  		obj.File = absPath(details.WorkingDir, obj.File)
   661  	}
   662  
   663  	return obj, nil
   664  }
   665  
   666  func absPath(workingDir string, filePath string) string {
   667  	if filepath.IsAbs(filePath) {
   668  		return filePath
   669  	}
   670  	return filepath.Join(workingDir, filePath)
   671  }
   672  
   673  func transformMapStringString(data interface{}) (interface{}, error) {
   674  	switch value := data.(type) {
   675  	case map[string]interface{}:
   676  		return toMapStringString(value, false), nil
   677  	case map[string]string:
   678  		return value, nil
   679  	default:
   680  		return data, errors.Errorf("invalid type %T for map[string]string", value)
   681  	}
   682  }
   683  
   684  func transformExternal(data interface{}) (interface{}, error) {
   685  	switch value := data.(type) {
   686  	case bool:
   687  		return map[string]interface{}{"external": value}, nil
   688  	case map[string]interface{}:
   689  		return map[string]interface{}{"external": true, "name": value["name"]}, nil
   690  	default:
   691  		return data, errors.Errorf("invalid type %T for external", value)
   692  	}
   693  }
   694  
   695  func transformServicePort(data interface{}) (interface{}, error) {
   696  	switch entries := data.(type) {
   697  	case []interface{}:
   698  		// We process the list instead of individual items here.
   699  		// The reason is that one entry might be mapped to multiple ServicePortConfig.
   700  		// Therefore we take an input of a list and return an output of a list.
   701  		ports := []interface{}{}
   702  		for _, entry := range entries {
   703  			switch value := entry.(type) {
   704  			case int:
   705  				v, err := toServicePortConfigs(fmt.Sprint(value))
   706  				if err != nil {
   707  					return data, err
   708  				}
   709  				ports = append(ports, v...)
   710  			case string:
   711  				v, err := toServicePortConfigs(value)
   712  				if err != nil {
   713  					return data, err
   714  				}
   715  				ports = append(ports, v...)
   716  			case map[string]interface{}:
   717  				ports = append(ports, value)
   718  			default:
   719  				return data, errors.Errorf("invalid type %T for port", value)
   720  			}
   721  		}
   722  		return ports, nil
   723  	default:
   724  		return data, errors.Errorf("invalid type %T for port", entries)
   725  	}
   726  }
   727  
   728  func transformStringSourceMap(data interface{}) (interface{}, error) {
   729  	switch value := data.(type) {
   730  	case string:
   731  		return map[string]interface{}{"source": value}, nil
   732  	case map[string]interface{}:
   733  		return data, nil
   734  	default:
   735  		return data, errors.Errorf("invalid type %T for secret", value)
   736  	}
   737  }
   738  
   739  func transformBuildConfig(data interface{}) (interface{}, error) {
   740  	switch value := data.(type) {
   741  	case string:
   742  		return map[string]interface{}{"context": value}, nil
   743  	case map[string]interface{}:
   744  		return data, nil
   745  	default:
   746  		return data, errors.Errorf("invalid type %T for service build", value)
   747  	}
   748  }
   749  
   750  func transformServiceVolumeConfig(data interface{}) (interface{}, error) {
   751  	switch value := data.(type) {
   752  	case string:
   753  		return ParseVolume(value)
   754  	case map[string]interface{}:
   755  		return data, nil
   756  	default:
   757  		return data, errors.Errorf("invalid type %T for service volume", value)
   758  	}
   759  }
   760  
   761  func transformServiceNetworkMap(value interface{}) (interface{}, error) {
   762  	if list, ok := value.([]interface{}); ok {
   763  		mapValue := map[interface{}]interface{}{}
   764  		for _, name := range list {
   765  			mapValue[name] = nil
   766  		}
   767  		return mapValue, nil
   768  	}
   769  	return value, nil
   770  }
   771  
   772  func transformStringOrNumberList(value interface{}) (interface{}, error) {
   773  	list := value.([]interface{})
   774  	result := make([]string, len(list))
   775  	for i, item := range list {
   776  		result[i] = fmt.Sprint(item)
   777  	}
   778  	return result, nil
   779  }
   780  
   781  func transformStringList(data interface{}) (interface{}, error) {
   782  	switch value := data.(type) {
   783  	case string:
   784  		return []string{value}, nil
   785  	case []interface{}:
   786  		return value, nil
   787  	default:
   788  		return data, errors.Errorf("invalid type %T for string list", value)
   789  	}
   790  }
   791  
   792  func transformMappingOrListFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) {
   793  	return func(data interface{}) (interface{}, error) {
   794  		return transformMappingOrList(data, sep, allowNil), nil
   795  	}
   796  }
   797  
   798  func transformListOrMappingFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) {
   799  	return func(data interface{}) (interface{}, error) {
   800  		return transformListOrMapping(data, sep, allowNil), nil
   801  	}
   802  }
   803  
   804  func transformListOrMapping(listOrMapping interface{}, sep string, allowNil bool) interface{} {
   805  	switch value := listOrMapping.(type) {
   806  	case map[string]interface{}:
   807  		return toStringList(value, sep, allowNil)
   808  	case []interface{}:
   809  		return listOrMapping
   810  	}
   811  	panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping))
   812  }
   813  
   814  func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} {
   815  	switch value := mappingOrList.(type) {
   816  	case map[string]interface{}:
   817  		return toMapStringString(value, allowNil)
   818  	case ([]interface{}):
   819  		result := make(map[string]interface{})
   820  		for _, value := range value {
   821  			parts := strings.SplitN(value.(string), sep, 2)
   822  			key := parts[0]
   823  			switch {
   824  			case len(parts) == 1 && allowNil:
   825  				result[key] = nil
   826  			case len(parts) == 1 && !allowNil:
   827  				result[key] = ""
   828  			default:
   829  				result[key] = parts[1]
   830  			}
   831  		}
   832  		return result
   833  	}
   834  	panic(errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList))
   835  }
   836  
   837  func transformShellCommand(value interface{}) (interface{}, error) {
   838  	if str, ok := value.(string); ok {
   839  		return shellwords.Parse(str)
   840  	}
   841  	return value, nil
   842  }
   843  
   844  func transformHealthCheckTest(data interface{}) (interface{}, error) {
   845  	switch value := data.(type) {
   846  	case string:
   847  		return append([]string{"CMD-SHELL"}, value), nil
   848  	case []interface{}:
   849  		return value, nil
   850  	default:
   851  		return value, errors.Errorf("invalid type %T for healthcheck.test", value)
   852  	}
   853  }
   854  
   855  func transformSize(value interface{}) (interface{}, error) {
   856  	switch value := value.(type) {
   857  	case int:
   858  		return int64(value), nil
   859  	case string:
   860  		return units.RAMInBytes(value)
   861  	}
   862  	panic(errors.Errorf("invalid type for size %T", value))
   863  }
   864  
   865  func transformStringToDuration(value interface{}) (interface{}, error) {
   866  	switch value := value.(type) {
   867  	case string:
   868  		d, err := time.ParseDuration(value)
   869  		if err != nil {
   870  			return value, err
   871  		}
   872  		return types.Duration(d), nil
   873  	default:
   874  		return value, errors.Errorf("invalid type %T for duration", value)
   875  	}
   876  }
   877  
   878  func toServicePortConfigs(value string) ([]interface{}, error) {
   879  	var portConfigs []interface{}
   880  
   881  	ports, portBindings, err := nat.ParsePortSpecs([]string{value})
   882  	if err != nil {
   883  		return nil, err
   884  	}
   885  	// We need to sort the key of the ports to make sure it is consistent
   886  	keys := []string{}
   887  	for port := range ports {
   888  		keys = append(keys, string(port))
   889  	}
   890  	sort.Strings(keys)
   891  
   892  	for _, key := range keys {
   893  		// Reuse ConvertPortToPortConfig so that it is consistent
   894  		portConfig, err := opts.ConvertPortToPortConfig(nat.Port(key), portBindings)
   895  		if err != nil {
   896  			return nil, err
   897  		}
   898  		for _, p := range portConfig {
   899  			portConfigs = append(portConfigs, types.ServicePortConfig{
   900  				Protocol:  string(p.Protocol),
   901  				Target:    p.TargetPort,
   902  				Published: p.PublishedPort,
   903  				Mode:      string(p.PublishMode),
   904  			})
   905  		}
   906  	}
   907  
   908  	return portConfigs, nil
   909  }
   910  
   911  func toMapStringString(value map[string]interface{}, allowNil bool) map[string]interface{} {
   912  	output := make(map[string]interface{})
   913  	for key, value := range value {
   914  		output[key] = toString(value, allowNil)
   915  	}
   916  	return output
   917  }
   918  
   919  func toString(value interface{}, allowNil bool) interface{} {
   920  	switch {
   921  	case value != nil:
   922  		return fmt.Sprint(value)
   923  	case allowNil:
   924  		return nil
   925  	default:
   926  		return ""
   927  	}
   928  }
   929  
   930  func toStringList(value map[string]interface{}, separator string, allowNil bool) []string {
   931  	output := []string{}
   932  	for key, value := range value {
   933  		if value == nil && !allowNil {
   934  			continue
   935  		}
   936  		output = append(output, fmt.Sprintf("%s%s%s", key, separator, value))
   937  	}
   938  	sort.Strings(output)
   939  	return output
   940  }