github.com/AliyunContainerService/cli@v0.0.0-20181009023821-814ced4b30d0/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.MappingWithEquals{}):                transformMappingOrListFunc("=", true),
   308  		reflect.TypeOf(types.Labels{}):                           transformMappingOrListFunc("=", false),
   309  		reflect.TypeOf(types.MappingWithColon{}):                 transformMappingOrListFunc(":", false),
   310  		reflect.TypeOf(types.HostsList{}):                        transformListOrMappingFunc(":", false),
   311  		reflect.TypeOf(types.ServiceVolumeConfig{}):              transformServiceVolumeConfig,
   312  		reflect.TypeOf(types.BuildConfig{}):                      transformBuildConfig,
   313  		reflect.TypeOf(types.Duration(0)):                        transformStringToDuration,
   314  	}
   315  
   316  	for _, transformer := range additionalTransformers {
   317  		transforms[transformer.TypeOf] = transformer.Func
   318  	}
   319  
   320  	return func(_ reflect.Type, target reflect.Type, data interface{}) (interface{}, error) {
   321  		transform, ok := transforms[target]
   322  		if !ok {
   323  			return data, nil
   324  		}
   325  		return transform(data)
   326  	}
   327  }
   328  
   329  // keys needs to be converted to strings for jsonschema
   330  func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) {
   331  	if mapping, ok := value.(map[interface{}]interface{}); ok {
   332  		dict := make(map[string]interface{})
   333  		for key, entry := range mapping {
   334  			str, ok := key.(string)
   335  			if !ok {
   336  				return nil, formatInvalidKeyError(keyPrefix, key)
   337  			}
   338  			var newKeyPrefix string
   339  			if keyPrefix == "" {
   340  				newKeyPrefix = str
   341  			} else {
   342  				newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str)
   343  			}
   344  			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
   345  			if err != nil {
   346  				return nil, err
   347  			}
   348  			dict[str] = convertedEntry
   349  		}
   350  		return dict, nil
   351  	}
   352  	if list, ok := value.([]interface{}); ok {
   353  		var convertedList []interface{}
   354  		for index, entry := range list {
   355  			newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index)
   356  			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
   357  			if err != nil {
   358  				return nil, err
   359  			}
   360  			convertedList = append(convertedList, convertedEntry)
   361  		}
   362  		return convertedList, nil
   363  	}
   364  	return value, nil
   365  }
   366  
   367  func formatInvalidKeyError(keyPrefix string, key interface{}) error {
   368  	var location string
   369  	if keyPrefix == "" {
   370  		location = "at top level"
   371  	} else {
   372  		location = fmt.Sprintf("in %s", keyPrefix)
   373  	}
   374  	return errors.Errorf("Non-string key %s: %#v", location, key)
   375  }
   376  
   377  // LoadServices produces a ServiceConfig map from a compose file Dict
   378  // the servicesDict is not validated if directly used. Use Load() to enable validation
   379  func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) {
   380  	var services []types.ServiceConfig
   381  
   382  	for name, serviceDef := range servicesDict {
   383  		serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, lookupEnv)
   384  		if err != nil {
   385  			return nil, err
   386  		}
   387  		services = append(services, *serviceConfig)
   388  	}
   389  
   390  	return services, nil
   391  }
   392  
   393  // LoadService produces a single ServiceConfig from a compose file Dict
   394  // the serviceDict is not validated if directly used. Use Load() to enable validation
   395  func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) {
   396  	serviceConfig := &types.ServiceConfig{}
   397  	if err := Transform(serviceDict, serviceConfig); err != nil {
   398  		return nil, err
   399  	}
   400  	serviceConfig.Name = name
   401  
   402  	if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil {
   403  		return nil, err
   404  	}
   405  
   406  	if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil {
   407  		return nil, err
   408  	}
   409  
   410  	serviceConfig.Extras = getExtras(serviceDict)
   411  
   412  	return serviceConfig, nil
   413  }
   414  
   415  func loadExtras(name string, source map[string]interface{}) map[string]interface{} {
   416  	if dict, ok := source[name].(map[string]interface{}); ok {
   417  		return getExtras(dict)
   418  	}
   419  	return nil
   420  }
   421  
   422  func getExtras(dict map[string]interface{}) map[string]interface{} {
   423  	extras := map[string]interface{}{}
   424  	for key, value := range dict {
   425  		if strings.HasPrefix(key, "x-") {
   426  			extras[key] = value
   427  		}
   428  	}
   429  	if len(extras) == 0 {
   430  		return nil
   431  	}
   432  	return extras
   433  }
   434  
   435  func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv template.Mapping) {
   436  	for k, v := range vars {
   437  		interpolatedV, ok := lookupEnv(k)
   438  		if (v == nil || *v == "") && ok {
   439  			// lookupEnv is prioritized over vars
   440  			environment[k] = &interpolatedV
   441  		} else {
   442  			environment[k] = v
   443  		}
   444  	}
   445  }
   446  
   447  func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error {
   448  	environment := make(map[string]*string)
   449  
   450  	if len(serviceConfig.EnvFile) > 0 {
   451  		var envVars []string
   452  
   453  		for _, file := range serviceConfig.EnvFile {
   454  			filePath := absPath(workingDir, file)
   455  			fileVars, err := opts.ParseEnvFile(filePath)
   456  			if err != nil {
   457  				return err
   458  			}
   459  			envVars = append(envVars, fileVars...)
   460  		}
   461  		updateEnvironment(environment,
   462  			opts.ConvertKVStringsToMapWithNil(envVars), lookupEnv)
   463  	}
   464  
   465  	updateEnvironment(environment, serviceConfig.Environment, lookupEnv)
   466  	serviceConfig.Environment = environment
   467  	return nil
   468  }
   469  
   470  func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error {
   471  	for i, volume := range volumes {
   472  		if volume.Type != "bind" {
   473  			continue
   474  		}
   475  
   476  		if volume.Source == "" {
   477  			return errors.New(`invalid mount config for type "bind": field Source must not be empty`)
   478  		}
   479  
   480  		filePath := expandUser(volume.Source, lookupEnv)
   481  		// Check for a Unix absolute path first, to handle a Windows client
   482  		// with a Unix daemon. This handles a Windows client connecting to a
   483  		// Unix daemon. Note that this is not required for Docker for Windows
   484  		// when specifying a local Windows path, because Docker for Windows
   485  		// translates the Windows path into a valid path within the VM.
   486  		if !path.IsAbs(filePath) {
   487  			filePath = absPath(workingDir, filePath)
   488  		}
   489  		volume.Source = filePath
   490  		volumes[i] = volume
   491  	}
   492  	return nil
   493  }
   494  
   495  // TODO: make this more robust
   496  func expandUser(path string, lookupEnv template.Mapping) string {
   497  	if strings.HasPrefix(path, "~") {
   498  		home, ok := lookupEnv("HOME")
   499  		if !ok {
   500  			logrus.Warn("cannot expand '~', because the environment lacks HOME")
   501  			return path
   502  		}
   503  		return strings.Replace(path, "~", home, 1)
   504  	}
   505  	return path
   506  }
   507  
   508  func transformUlimits(data interface{}) (interface{}, error) {
   509  	switch value := data.(type) {
   510  	case int:
   511  		return types.UlimitsConfig{Single: value}, nil
   512  	case map[string]interface{}:
   513  		ulimit := types.UlimitsConfig{}
   514  		ulimit.Soft = value["soft"].(int)
   515  		ulimit.Hard = value["hard"].(int)
   516  		return ulimit, nil
   517  	default:
   518  		return data, errors.Errorf("invalid type %T for ulimits", value)
   519  	}
   520  }
   521  
   522  // LoadNetworks produces a NetworkConfig map from a compose file Dict
   523  // the source Dict is not validated if directly used. Use Load() to enable validation
   524  func LoadNetworks(source map[string]interface{}, version string) (map[string]types.NetworkConfig, error) {
   525  	networks := make(map[string]types.NetworkConfig)
   526  	err := Transform(source, &networks)
   527  	if err != nil {
   528  		return networks, err
   529  	}
   530  	for name, network := range networks {
   531  		if !network.External.External {
   532  			continue
   533  		}
   534  		switch {
   535  		case network.External.Name != "":
   536  			if network.Name != "" {
   537  				return nil, errors.Errorf("network %s: network.external.name and network.name conflict; only use network.name", name)
   538  			}
   539  			if versions.GreaterThanOrEqualTo(version, "3.5") {
   540  				logrus.Warnf("network %s: network.external.name is deprecated in favor of network.name", name)
   541  			}
   542  			network.Name = network.External.Name
   543  			network.External.Name = ""
   544  		case network.Name == "":
   545  			network.Name = name
   546  		}
   547  		network.Extras = loadExtras(name, source)
   548  		networks[name] = network
   549  	}
   550  	return networks, nil
   551  }
   552  
   553  func externalVolumeError(volume, key string) error {
   554  	return errors.Errorf(
   555  		"conflicting parameters \"external\" and %q specified for volume %q",
   556  		key, volume)
   557  }
   558  
   559  // LoadVolumes produces a VolumeConfig map from a compose file Dict
   560  // the source Dict is not validated if directly used. Use Load() to enable validation
   561  func LoadVolumes(source map[string]interface{}, version string) (map[string]types.VolumeConfig, error) {
   562  	volumes := make(map[string]types.VolumeConfig)
   563  	if err := Transform(source, &volumes); err != nil {
   564  		return volumes, err
   565  	}
   566  
   567  	for name, volume := range volumes {
   568  		if !volume.External.External {
   569  			continue
   570  		}
   571  		switch {
   572  		case volume.Driver != "":
   573  			return nil, externalVolumeError(name, "driver")
   574  		case len(volume.DriverOpts) > 0:
   575  			return nil, externalVolumeError(name, "driver_opts")
   576  		case len(volume.Labels) > 0:
   577  			return nil, externalVolumeError(name, "labels")
   578  		case volume.External.Name != "":
   579  			if volume.Name != "" {
   580  				return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name)
   581  			}
   582  			if versions.GreaterThanOrEqualTo(version, "3.4") {
   583  				logrus.Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name)
   584  			}
   585  			volume.Name = volume.External.Name
   586  			volume.External.Name = ""
   587  		case volume.Name == "":
   588  			volume.Name = name
   589  		}
   590  		volume.Extras = loadExtras(name, source)
   591  		volumes[name] = volume
   592  	}
   593  	return volumes, nil
   594  }
   595  
   596  // LoadSecrets produces a SecretConfig map from a compose file Dict
   597  // the source Dict is not validated if directly used. Use Load() to enable validation
   598  func LoadSecrets(source map[string]interface{}, details types.ConfigDetails) (map[string]types.SecretConfig, error) {
   599  	secrets := make(map[string]types.SecretConfig)
   600  	if err := Transform(source, &secrets); err != nil {
   601  		return secrets, err
   602  	}
   603  	for name, secret := range secrets {
   604  		obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details)
   605  		if err != nil {
   606  			return nil, err
   607  		}
   608  		secretConfig := types.SecretConfig(obj)
   609  		secretConfig.Extras = loadExtras(name, source)
   610  		secrets[name] = secretConfig
   611  	}
   612  	return secrets, nil
   613  }
   614  
   615  // LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict
   616  // the source Dict is not validated if directly used. Use Load() to enable validation
   617  func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails) (map[string]types.ConfigObjConfig, error) {
   618  	configs := make(map[string]types.ConfigObjConfig)
   619  	if err := Transform(source, &configs); err != nil {
   620  		return configs, err
   621  	}
   622  	for name, config := range configs {
   623  		obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details)
   624  		if err != nil {
   625  			return nil, err
   626  		}
   627  		configConfig := types.ConfigObjConfig(obj)
   628  		configConfig.Extras = loadExtras(name, source)
   629  		configs[name] = configConfig
   630  	}
   631  	return configs, nil
   632  }
   633  
   634  func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails) (types.FileObjectConfig, error) {
   635  	// if "external: true"
   636  	if obj.External.External {
   637  		// handle deprecated external.name
   638  		if obj.External.Name != "" {
   639  			if obj.Name != "" {
   640  				return obj, errors.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name)
   641  			}
   642  			if versions.GreaterThanOrEqualTo(details.Version, "3.5") {
   643  				logrus.Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name)
   644  			}
   645  			obj.Name = obj.External.Name
   646  			obj.External.Name = ""
   647  		} else {
   648  			if obj.Name == "" {
   649  				obj.Name = name
   650  			}
   651  		}
   652  		// if not "external: true"
   653  	} else {
   654  		obj.File = absPath(details.WorkingDir, obj.File)
   655  	}
   656  
   657  	return obj, nil
   658  }
   659  
   660  func absPath(workingDir string, filePath string) string {
   661  	if filepath.IsAbs(filePath) {
   662  		return filePath
   663  	}
   664  	return filepath.Join(workingDir, filePath)
   665  }
   666  
   667  func transformMapStringString(data interface{}) (interface{}, error) {
   668  	switch value := data.(type) {
   669  	case map[string]interface{}:
   670  		return toMapStringString(value, false), nil
   671  	case map[string]string:
   672  		return value, nil
   673  	default:
   674  		return data, errors.Errorf("invalid type %T for map[string]string", value)
   675  	}
   676  }
   677  
   678  func transformExternal(data interface{}) (interface{}, error) {
   679  	switch value := data.(type) {
   680  	case bool:
   681  		return map[string]interface{}{"external": value}, nil
   682  	case map[string]interface{}:
   683  		return map[string]interface{}{"external": true, "name": value["name"]}, nil
   684  	default:
   685  		return data, errors.Errorf("invalid type %T for external", value)
   686  	}
   687  }
   688  
   689  func transformServicePort(data interface{}) (interface{}, error) {
   690  	switch entries := data.(type) {
   691  	case []interface{}:
   692  		// We process the list instead of individual items here.
   693  		// The reason is that one entry might be mapped to multiple ServicePortConfig.
   694  		// Therefore we take an input of a list and return an output of a list.
   695  		ports := []interface{}{}
   696  		for _, entry := range entries {
   697  			switch value := entry.(type) {
   698  			case int:
   699  				v, err := toServicePortConfigs(fmt.Sprint(value))
   700  				if err != nil {
   701  					return data, err
   702  				}
   703  				ports = append(ports, v...)
   704  			case string:
   705  				v, err := toServicePortConfigs(value)
   706  				if err != nil {
   707  					return data, err
   708  				}
   709  				ports = append(ports, v...)
   710  			case map[string]interface{}:
   711  				ports = append(ports, value)
   712  			default:
   713  				return data, errors.Errorf("invalid type %T for port", value)
   714  			}
   715  		}
   716  		return ports, nil
   717  	default:
   718  		return data, errors.Errorf("invalid type %T for port", entries)
   719  	}
   720  }
   721  
   722  func transformStringSourceMap(data interface{}) (interface{}, error) {
   723  	switch value := data.(type) {
   724  	case string:
   725  		return map[string]interface{}{"source": value}, nil
   726  	case map[string]interface{}:
   727  		return data, nil
   728  	default:
   729  		return data, errors.Errorf("invalid type %T for secret", value)
   730  	}
   731  }
   732  
   733  func transformBuildConfig(data interface{}) (interface{}, error) {
   734  	switch value := data.(type) {
   735  	case string:
   736  		return map[string]interface{}{"context": value}, nil
   737  	case map[string]interface{}:
   738  		return data, nil
   739  	default:
   740  		return data, errors.Errorf("invalid type %T for service build", value)
   741  	}
   742  }
   743  
   744  func transformServiceVolumeConfig(data interface{}) (interface{}, error) {
   745  	switch value := data.(type) {
   746  	case string:
   747  		return ParseVolume(value)
   748  	case map[string]interface{}:
   749  		return data, nil
   750  	default:
   751  		return data, errors.Errorf("invalid type %T for service volume", value)
   752  	}
   753  }
   754  
   755  func transformServiceNetworkMap(value interface{}) (interface{}, error) {
   756  	if list, ok := value.([]interface{}); ok {
   757  		mapValue := map[interface{}]interface{}{}
   758  		for _, name := range list {
   759  			mapValue[name] = nil
   760  		}
   761  		return mapValue, nil
   762  	}
   763  	return value, nil
   764  }
   765  
   766  func transformStringOrNumberList(value interface{}) (interface{}, error) {
   767  	list := value.([]interface{})
   768  	result := make([]string, len(list))
   769  	for i, item := range list {
   770  		result[i] = fmt.Sprint(item)
   771  	}
   772  	return result, nil
   773  }
   774  
   775  func transformStringList(data interface{}) (interface{}, error) {
   776  	switch value := data.(type) {
   777  	case string:
   778  		return []string{value}, nil
   779  	case []interface{}:
   780  		return value, nil
   781  	default:
   782  		return data, errors.Errorf("invalid type %T for string list", value)
   783  	}
   784  }
   785  
   786  func transformMappingOrListFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) {
   787  	return func(data interface{}) (interface{}, error) {
   788  		return transformMappingOrList(data, sep, allowNil), nil
   789  	}
   790  }
   791  
   792  func transformListOrMappingFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) {
   793  	return func(data interface{}) (interface{}, error) {
   794  		return transformListOrMapping(data, sep, allowNil), nil
   795  	}
   796  }
   797  
   798  func transformListOrMapping(listOrMapping interface{}, sep string, allowNil bool) interface{} {
   799  	switch value := listOrMapping.(type) {
   800  	case map[string]interface{}:
   801  		return toStringList(value, sep, allowNil)
   802  	case []interface{}:
   803  		return listOrMapping
   804  	}
   805  	panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping))
   806  }
   807  
   808  func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} {
   809  	switch value := mappingOrList.(type) {
   810  	case map[string]interface{}:
   811  		return toMapStringString(value, allowNil)
   812  	case ([]interface{}):
   813  		result := make(map[string]interface{})
   814  		for _, value := range value {
   815  			parts := strings.SplitN(value.(string), sep, 2)
   816  			key := parts[0]
   817  			switch {
   818  			case len(parts) == 1 && allowNil:
   819  				result[key] = nil
   820  			case len(parts) == 1 && !allowNil:
   821  				result[key] = ""
   822  			default:
   823  				result[key] = parts[1]
   824  			}
   825  		}
   826  		return result
   827  	}
   828  	panic(errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList))
   829  }
   830  
   831  func transformShellCommand(value interface{}) (interface{}, error) {
   832  	if str, ok := value.(string); ok {
   833  		return shellwords.Parse(str)
   834  	}
   835  	return value, nil
   836  }
   837  
   838  func transformHealthCheckTest(data interface{}) (interface{}, error) {
   839  	switch value := data.(type) {
   840  	case string:
   841  		return append([]string{"CMD-SHELL"}, value), nil
   842  	case []interface{}:
   843  		return value, nil
   844  	default:
   845  		return value, errors.Errorf("invalid type %T for healthcheck.test", value)
   846  	}
   847  }
   848  
   849  func transformSize(value interface{}) (interface{}, error) {
   850  	switch value := value.(type) {
   851  	case int:
   852  		return int64(value), nil
   853  	case string:
   854  		return units.RAMInBytes(value)
   855  	}
   856  	panic(errors.Errorf("invalid type for size %T", value))
   857  }
   858  
   859  func transformStringToDuration(value interface{}) (interface{}, error) {
   860  	switch value := value.(type) {
   861  	case string:
   862  		d, err := time.ParseDuration(value)
   863  		if err != nil {
   864  			return value, err
   865  		}
   866  		return types.Duration(d), nil
   867  	default:
   868  		return value, errors.Errorf("invalid type %T for duration", value)
   869  	}
   870  }
   871  
   872  func toServicePortConfigs(value string) ([]interface{}, error) {
   873  	var portConfigs []interface{}
   874  
   875  	ports, portBindings, err := nat.ParsePortSpecs([]string{value})
   876  	if err != nil {
   877  		return nil, err
   878  	}
   879  	// We need to sort the key of the ports to make sure it is consistent
   880  	keys := []string{}
   881  	for port := range ports {
   882  		keys = append(keys, string(port))
   883  	}
   884  	sort.Strings(keys)
   885  
   886  	for _, key := range keys {
   887  		// Reuse ConvertPortToPortConfig so that it is consistent
   888  		portConfig, err := opts.ConvertPortToPortConfig(nat.Port(key), portBindings)
   889  		if err != nil {
   890  			return nil, err
   891  		}
   892  		for _, p := range portConfig {
   893  			portConfigs = append(portConfigs, types.ServicePortConfig{
   894  				Protocol:  string(p.Protocol),
   895  				Target:    p.TargetPort,
   896  				Published: p.PublishedPort,
   897  				Mode:      string(p.PublishMode),
   898  			})
   899  		}
   900  	}
   901  
   902  	return portConfigs, nil
   903  }
   904  
   905  func toMapStringString(value map[string]interface{}, allowNil bool) map[string]interface{} {
   906  	output := make(map[string]interface{})
   907  	for key, value := range value {
   908  		output[key] = toString(value, allowNil)
   909  	}
   910  	return output
   911  }
   912  
   913  func toString(value interface{}, allowNil bool) interface{} {
   914  	switch {
   915  	case value != nil:
   916  		return fmt.Sprint(value)
   917  	case allowNil:
   918  		return nil
   919  	default:
   920  		return ""
   921  	}
   922  }
   923  
   924  func toStringList(value map[string]interface{}, separator string, allowNil bool) []string {
   925  	output := []string{}
   926  	for key, value := range value {
   927  		if value == nil && !allowNil {
   928  			continue
   929  		}
   930  		output = append(output, fmt.Sprintf("%s%s%s", key, separator, value))
   931  	}
   932  	sort.Strings(output)
   933  	return output
   934  }