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