github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/compose/loader/loader.go (about)

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