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