github.com/kunnos/engine@v1.13.1/cli/compose/loader/loader.go (about)

     1  package loader
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path"
     7  	"reflect"
     8  	"regexp"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/docker/docker/cli/compose/interpolation"
    13  	"github.com/docker/docker/cli/compose/schema"
    14  	"github.com/docker/docker/cli/compose/types"
    15  	"github.com/docker/docker/runconfig/opts"
    16  	units "github.com/docker/go-units"
    17  	shellwords "github.com/mattn/go-shellwords"
    18  	"github.com/mitchellh/mapstructure"
    19  	yaml "gopkg.in/yaml.v2"
    20  )
    21  
    22  var (
    23  	fieldNameRegexp = regexp.MustCompile("[A-Z][a-z0-9]+")
    24  )
    25  
    26  // ParseYAML reads the bytes from a file, parses the bytes into a mapping
    27  // structure, and returns it.
    28  func ParseYAML(source []byte) (types.Dict, error) {
    29  	var cfg interface{}
    30  	if err := yaml.Unmarshal(source, &cfg); err != nil {
    31  		return nil, err
    32  	}
    33  	cfgMap, ok := cfg.(map[interface{}]interface{})
    34  	if !ok {
    35  		return nil, fmt.Errorf("Top-level object must be a mapping")
    36  	}
    37  	converted, err := convertToStringKeysRecursive(cfgMap, "")
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  	return converted.(types.Dict), nil
    42  }
    43  
    44  // Load reads a ConfigDetails and returns a fully loaded configuration
    45  func Load(configDetails types.ConfigDetails) (*types.Config, error) {
    46  	if len(configDetails.ConfigFiles) < 1 {
    47  		return nil, fmt.Errorf("No files specified")
    48  	}
    49  	if len(configDetails.ConfigFiles) > 1 {
    50  		return nil, fmt.Errorf("Multiple files are not yet supported")
    51  	}
    52  
    53  	configDict := getConfigDict(configDetails)
    54  
    55  	if services, ok := configDict["services"]; ok {
    56  		if servicesDict, ok := services.(types.Dict); ok {
    57  			forbidden := getProperties(servicesDict, types.ForbiddenProperties)
    58  
    59  			if len(forbidden) > 0 {
    60  				return nil, &ForbiddenPropertiesError{Properties: forbidden}
    61  			}
    62  		}
    63  	}
    64  
    65  	if err := schema.Validate(configDict, schema.Version(configDict)); err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	cfg := types.Config{}
    70  	if services, ok := configDict["services"]; ok {
    71  		servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", os.LookupEnv)
    72  		if err != nil {
    73  			return nil, err
    74  		}
    75  
    76  		servicesList, err := loadServices(servicesConfig, configDetails.WorkingDir)
    77  		if err != nil {
    78  			return nil, err
    79  		}
    80  
    81  		cfg.Services = servicesList
    82  	}
    83  
    84  	if networks, ok := configDict["networks"]; ok {
    85  		networksConfig, err := interpolation.Interpolate(networks.(types.Dict), "network", os.LookupEnv)
    86  		if err != nil {
    87  			return nil, err
    88  		}
    89  
    90  		networksMapping, err := loadNetworks(networksConfig)
    91  		if err != nil {
    92  			return nil, err
    93  		}
    94  
    95  		cfg.Networks = networksMapping
    96  	}
    97  
    98  	if volumes, ok := configDict["volumes"]; ok {
    99  		volumesConfig, err := interpolation.Interpolate(volumes.(types.Dict), "volume", os.LookupEnv)
   100  		if err != nil {
   101  			return nil, err
   102  		}
   103  
   104  		volumesMapping, err := loadVolumes(volumesConfig)
   105  		if err != nil {
   106  			return nil, err
   107  		}
   108  
   109  		cfg.Volumes = volumesMapping
   110  	}
   111  
   112  	if secrets, ok := configDict["secrets"]; ok {
   113  		secretsConfig, err := interpolation.Interpolate(secrets.(types.Dict), "secret", os.LookupEnv)
   114  		if err != nil {
   115  			return nil, err
   116  		}
   117  
   118  		secretsMapping, err := loadSecrets(secretsConfig, configDetails.WorkingDir)
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  
   123  		cfg.Secrets = secretsMapping
   124  	}
   125  
   126  	return &cfg, nil
   127  }
   128  
   129  // GetUnsupportedProperties returns the list of any unsupported properties that are
   130  // used in the Compose files.
   131  func GetUnsupportedProperties(configDetails types.ConfigDetails) []string {
   132  	unsupported := map[string]bool{}
   133  
   134  	for _, service := range getServices(getConfigDict(configDetails)) {
   135  		serviceDict := service.(types.Dict)
   136  		for _, property := range types.UnsupportedProperties {
   137  			if _, isSet := serviceDict[property]; isSet {
   138  				unsupported[property] = true
   139  			}
   140  		}
   141  	}
   142  
   143  	return sortedKeys(unsupported)
   144  }
   145  
   146  func sortedKeys(set map[string]bool) []string {
   147  	var keys []string
   148  	for key := range set {
   149  		keys = append(keys, key)
   150  	}
   151  	sort.Strings(keys)
   152  	return keys
   153  }
   154  
   155  // GetDeprecatedProperties returns the list of any deprecated properties that
   156  // are used in the compose files.
   157  func GetDeprecatedProperties(configDetails types.ConfigDetails) map[string]string {
   158  	return getProperties(getServices(getConfigDict(configDetails)), types.DeprecatedProperties)
   159  }
   160  
   161  func getProperties(services types.Dict, propertyMap map[string]string) map[string]string {
   162  	output := map[string]string{}
   163  
   164  	for _, service := range services {
   165  		if serviceDict, ok := service.(types.Dict); ok {
   166  			for property, description := range propertyMap {
   167  				if _, isSet := serviceDict[property]; isSet {
   168  					output[property] = description
   169  				}
   170  			}
   171  		}
   172  	}
   173  
   174  	return output
   175  }
   176  
   177  // ForbiddenPropertiesError is returned when there are properties in the Compose
   178  // file that are forbidden.
   179  type ForbiddenPropertiesError struct {
   180  	Properties map[string]string
   181  }
   182  
   183  func (e *ForbiddenPropertiesError) Error() string {
   184  	return "Configuration contains forbidden properties"
   185  }
   186  
   187  // TODO: resolve multiple files into a single config
   188  func getConfigDict(configDetails types.ConfigDetails) types.Dict {
   189  	return configDetails.ConfigFiles[0].Config
   190  }
   191  
   192  func getServices(configDict types.Dict) types.Dict {
   193  	if services, ok := configDict["services"]; ok {
   194  		if servicesDict, ok := services.(types.Dict); ok {
   195  			return servicesDict
   196  		}
   197  	}
   198  
   199  	return types.Dict{}
   200  }
   201  
   202  func transform(source map[string]interface{}, target interface{}) error {
   203  	data := mapstructure.Metadata{}
   204  	config := &mapstructure.DecoderConfig{
   205  		DecodeHook: mapstructure.ComposeDecodeHookFunc(
   206  			transformHook,
   207  			mapstructure.StringToTimeDurationHookFunc()),
   208  		Result:   target,
   209  		Metadata: &data,
   210  	}
   211  	decoder, err := mapstructure.NewDecoder(config)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	err = decoder.Decode(source)
   216  	// TODO: log unused keys
   217  	return err
   218  }
   219  
   220  func transformHook(
   221  	source reflect.Type,
   222  	target reflect.Type,
   223  	data interface{},
   224  ) (interface{}, error) {
   225  	switch target {
   226  	case reflect.TypeOf(types.External{}):
   227  		return transformExternal(data)
   228  	case reflect.TypeOf(make(map[string]string, 0)):
   229  		return transformMapStringString(source, target, data)
   230  	case reflect.TypeOf(types.UlimitsConfig{}):
   231  		return transformUlimits(data)
   232  	case reflect.TypeOf(types.UnitBytes(0)):
   233  		return loadSize(data)
   234  	case reflect.TypeOf(types.ServiceSecretConfig{}):
   235  		return transformServiceSecret(data)
   236  	}
   237  	switch target.Kind() {
   238  	case reflect.Struct:
   239  		return transformStruct(source, target, data)
   240  	}
   241  	return data, nil
   242  }
   243  
   244  // keys needs to be converted to strings for jsonschema
   245  // TODO: don't use types.Dict
   246  func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) {
   247  	if mapping, ok := value.(map[interface{}]interface{}); ok {
   248  		dict := make(types.Dict)
   249  		for key, entry := range mapping {
   250  			str, ok := key.(string)
   251  			if !ok {
   252  				var location string
   253  				if keyPrefix == "" {
   254  					location = "at top level"
   255  				} else {
   256  					location = fmt.Sprintf("in %s", keyPrefix)
   257  				}
   258  				return nil, fmt.Errorf("Non-string key %s: %#v", location, key)
   259  			}
   260  			var newKeyPrefix string
   261  			if keyPrefix == "" {
   262  				newKeyPrefix = str
   263  			} else {
   264  				newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str)
   265  			}
   266  			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
   267  			if err != nil {
   268  				return nil, err
   269  			}
   270  			dict[str] = convertedEntry
   271  		}
   272  		return dict, nil
   273  	}
   274  	if list, ok := value.([]interface{}); ok {
   275  		var convertedList []interface{}
   276  		for index, entry := range list {
   277  			newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index)
   278  			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
   279  			if err != nil {
   280  				return nil, err
   281  			}
   282  			convertedList = append(convertedList, convertedEntry)
   283  		}
   284  		return convertedList, nil
   285  	}
   286  	return value, nil
   287  }
   288  
   289  func loadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceConfig, error) {
   290  	var services []types.ServiceConfig
   291  
   292  	for name, serviceDef := range servicesDict {
   293  		serviceConfig, err := loadService(name, serviceDef.(types.Dict), workingDir)
   294  		if err != nil {
   295  			return nil, err
   296  		}
   297  		services = append(services, *serviceConfig)
   298  	}
   299  
   300  	return services, nil
   301  }
   302  
   303  func loadService(name string, serviceDict types.Dict, workingDir string) (*types.ServiceConfig, error) {
   304  	serviceConfig := &types.ServiceConfig{}
   305  	if err := transform(serviceDict, serviceConfig); err != nil {
   306  		return nil, err
   307  	}
   308  	serviceConfig.Name = name
   309  
   310  	if err := resolveEnvironment(serviceConfig, serviceDict, workingDir); err != nil {
   311  		return nil, err
   312  	}
   313  
   314  	if err := resolveVolumePaths(serviceConfig.Volumes, workingDir); err != nil {
   315  		return nil, err
   316  	}
   317  
   318  	return serviceConfig, nil
   319  }
   320  
   321  func resolveEnvironment(serviceConfig *types.ServiceConfig, serviceDict types.Dict, workingDir string) error {
   322  	environment := make(map[string]string)
   323  
   324  	if envFileVal, ok := serviceDict["env_file"]; ok {
   325  		envFiles := loadStringOrListOfStrings(envFileVal)
   326  
   327  		var envVars []string
   328  
   329  		for _, file := range envFiles {
   330  			filePath := absPath(workingDir, file)
   331  			fileVars, err := opts.ParseEnvFile(filePath)
   332  			if err != nil {
   333  				return err
   334  			}
   335  			envVars = append(envVars, fileVars...)
   336  		}
   337  
   338  		for k, v := range opts.ConvertKVStringsToMap(envVars) {
   339  			environment[k] = v
   340  		}
   341  	}
   342  
   343  	for k, v := range serviceConfig.Environment {
   344  		environment[k] = v
   345  	}
   346  
   347  	serviceConfig.Environment = environment
   348  
   349  	return nil
   350  }
   351  
   352  func resolveVolumePaths(volumes []string, workingDir string) error {
   353  	for i, mapping := range volumes {
   354  		parts := strings.SplitN(mapping, ":", 2)
   355  		if len(parts) == 1 {
   356  			continue
   357  		}
   358  
   359  		if strings.HasPrefix(parts[0], ".") {
   360  			parts[0] = absPath(workingDir, parts[0])
   361  		}
   362  		parts[0] = expandUser(parts[0])
   363  
   364  		volumes[i] = strings.Join(parts, ":")
   365  	}
   366  
   367  	return nil
   368  }
   369  
   370  // TODO: make this more robust
   371  func expandUser(path string) string {
   372  	if strings.HasPrefix(path, "~") {
   373  		return strings.Replace(path, "~", os.Getenv("HOME"), 1)
   374  	}
   375  	return path
   376  }
   377  
   378  func transformUlimits(data interface{}) (interface{}, error) {
   379  	switch value := data.(type) {
   380  	case int:
   381  		return types.UlimitsConfig{Single: value}, nil
   382  	case types.Dict:
   383  		ulimit := types.UlimitsConfig{}
   384  		ulimit.Soft = value["soft"].(int)
   385  		ulimit.Hard = value["hard"].(int)
   386  		return ulimit, nil
   387  	default:
   388  		return data, fmt.Errorf("invalid type %T for ulimits", value)
   389  	}
   390  }
   391  
   392  func loadNetworks(source types.Dict) (map[string]types.NetworkConfig, error) {
   393  	networks := make(map[string]types.NetworkConfig)
   394  	err := transform(source, &networks)
   395  	if err != nil {
   396  		return networks, err
   397  	}
   398  	for name, network := range networks {
   399  		if network.External.External && network.External.Name == "" {
   400  			network.External.Name = name
   401  			networks[name] = network
   402  		}
   403  	}
   404  	return networks, nil
   405  }
   406  
   407  func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) {
   408  	volumes := make(map[string]types.VolumeConfig)
   409  	err := transform(source, &volumes)
   410  	if err != nil {
   411  		return volumes, err
   412  	}
   413  	for name, volume := range volumes {
   414  		if volume.External.External && volume.External.Name == "" {
   415  			volume.External.Name = name
   416  			volumes[name] = volume
   417  		}
   418  	}
   419  	return volumes, nil
   420  }
   421  
   422  // TODO: remove duplicate with networks/volumes
   423  func loadSecrets(source types.Dict, workingDir string) (map[string]types.SecretConfig, error) {
   424  	secrets := make(map[string]types.SecretConfig)
   425  	if err := transform(source, &secrets); err != nil {
   426  		return secrets, err
   427  	}
   428  	for name, secret := range secrets {
   429  		if secret.External.External && secret.External.Name == "" {
   430  			secret.External.Name = name
   431  			secrets[name] = secret
   432  		}
   433  		if secret.File != "" {
   434  			secret.File = absPath(workingDir, secret.File)
   435  		}
   436  	}
   437  	return secrets, nil
   438  }
   439  
   440  func absPath(workingDir string, filepath string) string {
   441  	if path.IsAbs(filepath) {
   442  		return filepath
   443  	}
   444  	return path.Join(workingDir, filepath)
   445  }
   446  
   447  func transformStruct(
   448  	source reflect.Type,
   449  	target reflect.Type,
   450  	data interface{},
   451  ) (interface{}, error) {
   452  	structValue, ok := data.(map[string]interface{})
   453  	if !ok {
   454  		// FIXME: this is necessary because of convertToStringKeysRecursive
   455  		structValue, ok = data.(types.Dict)
   456  		if !ok {
   457  			panic(fmt.Sprintf(
   458  				"transformStruct called with non-map type: %T, %s", data, data))
   459  		}
   460  	}
   461  
   462  	var err error
   463  	for i := 0; i < target.NumField(); i++ {
   464  		field := target.Field(i)
   465  		fieldTag := field.Tag.Get("compose")
   466  
   467  		yamlName := toYAMLName(field.Name)
   468  		value, ok := structValue[yamlName]
   469  		if !ok {
   470  			continue
   471  		}
   472  
   473  		structValue[yamlName], err = convertField(
   474  			fieldTag, reflect.TypeOf(value), field.Type, value)
   475  		if err != nil {
   476  			return nil, fmt.Errorf("field %s: %s", yamlName, err.Error())
   477  		}
   478  	}
   479  	return structValue, nil
   480  }
   481  
   482  func transformMapStringString(
   483  	source reflect.Type,
   484  	target reflect.Type,
   485  	data interface{},
   486  ) (interface{}, error) {
   487  	switch value := data.(type) {
   488  	case map[string]interface{}:
   489  		return toMapStringString(value), nil
   490  	case types.Dict:
   491  		return toMapStringString(value), nil
   492  	case map[string]string:
   493  		return value, nil
   494  	default:
   495  		return data, fmt.Errorf("invalid type %T for map[string]string", value)
   496  	}
   497  }
   498  
   499  func convertField(
   500  	fieldTag string,
   501  	source reflect.Type,
   502  	target reflect.Type,
   503  	data interface{},
   504  ) (interface{}, error) {
   505  	switch fieldTag {
   506  	case "":
   507  		return data, nil
   508  	case "healthcheck":
   509  		return loadHealthcheck(data)
   510  	case "list_or_dict_equals":
   511  		return loadMappingOrList(data, "="), nil
   512  	case "list_or_dict_colon":
   513  		return loadMappingOrList(data, ":"), nil
   514  	case "list_or_struct_map":
   515  		return loadListOrStructMap(data, target)
   516  	case "string_or_list":
   517  		return loadStringOrListOfStrings(data), nil
   518  	case "list_of_strings_or_numbers":
   519  		return loadListOfStringsOrNumbers(data), nil
   520  	case "shell_command":
   521  		return loadShellCommand(data)
   522  	case "size":
   523  		return loadSize(data)
   524  	case "-":
   525  		return nil, nil
   526  	}
   527  	return data, nil
   528  }
   529  
   530  func transformExternal(data interface{}) (interface{}, error) {
   531  	switch value := data.(type) {
   532  	case bool:
   533  		return map[string]interface{}{"external": value}, nil
   534  	case types.Dict:
   535  		return map[string]interface{}{"external": true, "name": value["name"]}, nil
   536  	case map[string]interface{}:
   537  		return map[string]interface{}{"external": true, "name": value["name"]}, nil
   538  	default:
   539  		return data, fmt.Errorf("invalid type %T for external", value)
   540  	}
   541  }
   542  
   543  func transformServiceSecret(data interface{}) (interface{}, error) {
   544  	switch value := data.(type) {
   545  	case string:
   546  		return map[string]interface{}{"source": value}, nil
   547  	case types.Dict:
   548  		return data, nil
   549  	case map[string]interface{}:
   550  		return data, nil
   551  	default:
   552  		return data, fmt.Errorf("invalid type %T for external", value)
   553  	}
   554  
   555  }
   556  
   557  func toYAMLName(name string) string {
   558  	nameParts := fieldNameRegexp.FindAllString(name, -1)
   559  	for i, p := range nameParts {
   560  		nameParts[i] = strings.ToLower(p)
   561  	}
   562  	return strings.Join(nameParts, "_")
   563  }
   564  
   565  func loadListOrStructMap(value interface{}, target reflect.Type) (interface{}, error) {
   566  	if list, ok := value.([]interface{}); ok {
   567  		mapValue := map[interface{}]interface{}{}
   568  		for _, name := range list {
   569  			mapValue[name] = nil
   570  		}
   571  		return mapValue, nil
   572  	}
   573  
   574  	return value, nil
   575  }
   576  
   577  func loadListOfStringsOrNumbers(value interface{}) []string {
   578  	list := value.([]interface{})
   579  	result := make([]string, len(list))
   580  	for i, item := range list {
   581  		result[i] = fmt.Sprint(item)
   582  	}
   583  	return result
   584  }
   585  
   586  func loadStringOrListOfStrings(value interface{}) []string {
   587  	if list, ok := value.([]interface{}); ok {
   588  		result := make([]string, len(list))
   589  		for i, item := range list {
   590  			result[i] = fmt.Sprint(item)
   591  		}
   592  		return result
   593  	}
   594  	return []string{value.(string)}
   595  }
   596  
   597  func loadMappingOrList(mappingOrList interface{}, sep string) map[string]string {
   598  	if mapping, ok := mappingOrList.(types.Dict); ok {
   599  		return toMapStringString(mapping)
   600  	}
   601  	if list, ok := mappingOrList.([]interface{}); ok {
   602  		result := make(map[string]string)
   603  		for _, value := range list {
   604  			parts := strings.SplitN(value.(string), sep, 2)
   605  			if len(parts) == 1 {
   606  				result[parts[0]] = ""
   607  			} else {
   608  				result[parts[0]] = parts[1]
   609  			}
   610  		}
   611  		return result
   612  	}
   613  	panic(fmt.Errorf("expected a map or a slice, got: %#v", mappingOrList))
   614  }
   615  
   616  func loadShellCommand(value interface{}) (interface{}, error) {
   617  	if str, ok := value.(string); ok {
   618  		return shellwords.Parse(str)
   619  	}
   620  	return value, nil
   621  }
   622  
   623  func loadHealthcheck(value interface{}) (interface{}, error) {
   624  	if str, ok := value.(string); ok {
   625  		return append([]string{"CMD-SHELL"}, str), nil
   626  	}
   627  	return value, nil
   628  }
   629  
   630  func loadSize(value interface{}) (int64, error) {
   631  	switch value := value.(type) {
   632  	case int:
   633  		return int64(value), nil
   634  	case string:
   635  		return units.RAMInBytes(value)
   636  	}
   637  	panic(fmt.Errorf("invalid type for size %T", value))
   638  }
   639  
   640  func toMapStringString(value map[string]interface{}) map[string]string {
   641  	output := make(map[string]string)
   642  	for key, value := range value {
   643  		output[key] = toString(value)
   644  	}
   645  	return output
   646  }
   647  
   648  func toString(value interface{}) string {
   649  	if value == nil {
   650  		return ""
   651  	}
   652  	return fmt.Sprint(value)
   653  }