github.com/ijc/docker-app@v0.6.1-0.20181012090447-c7ca8bc483fb/internal/helm/templateloader/loader.go (about)

     1  package templateloader
     2  
     3  import (
     4  	"fmt"
     5  	"path"
     6  	"path/filepath"
     7  	"reflect"
     8  	"sort"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/docker/app/internal/helm/templatetypes"
    14  	"github.com/docker/cli/cli/compose/loader"
    15  	"github.com/docker/cli/cli/compose/schema"
    16  	"github.com/docker/cli/cli/compose/template"
    17  	"github.com/docker/cli/cli/compose/types"
    18  	"github.com/docker/cli/opts"
    19  	"github.com/docker/go-connections/nat"
    20  	units "github.com/docker/go-units"
    21  	shellwords "github.com/mattn/go-shellwords"
    22  	"github.com/pkg/errors"
    23  	"github.com/sirupsen/logrus"
    24  )
    25  
    26  var (
    27  	transformers = []loader.Transformer{
    28  		{TypeOf: reflect.TypeOf(templatetypes.MappingWithEqualsTemplate{}), Func: transformMappingOrListFunc("=", true)},
    29  		{TypeOf: reflect.TypeOf(templatetypes.LabelsTemplate{}), Func: transformMappingOrListFunc("=", false)},
    30  		{TypeOf: reflect.TypeOf(templatetypes.HostsListTemplate{}), Func: transformHostsListTemplate},
    31  		{TypeOf: reflect.TypeOf(templatetypes.ShellCommandTemplate{}), Func: transformShellCommandTemplate},
    32  		{TypeOf: reflect.TypeOf(templatetypes.StringTemplateList{}), Func: transformStringTemplateList},
    33  		{TypeOf: reflect.TypeOf(templatetypes.StringTemplate{}), Func: transformStringTemplate},
    34  		{TypeOf: reflect.TypeOf(templatetypes.UnitBytesOrTemplate{}), Func: transformSize},
    35  		{TypeOf: reflect.TypeOf([]templatetypes.ServicePortConfig{}), Func: transformServicePort},
    36  		{TypeOf: reflect.TypeOf(templatetypes.ServiceSecretConfig{}), Func: transformStringSourceMap},
    37  		{TypeOf: reflect.TypeOf(templatetypes.ServiceConfigObjConfig{}), Func: transformStringSourceMap},
    38  		{TypeOf: reflect.TypeOf(templatetypes.ServiceVolumeConfig{}), Func: transformServiceVolumeConfig},
    39  		{TypeOf: reflect.TypeOf(templatetypes.BoolOrTemplate{}), Func: transformBoolOrTemplate},
    40  		{TypeOf: reflect.TypeOf(templatetypes.UInt64OrTemplate{}), Func: transformUInt64OrTemplate},
    41  		{TypeOf: reflect.TypeOf(templatetypes.DurationOrTemplate{}), Func: transformDurationOrTemplate},
    42  	}
    43  )
    44  
    45  // LoadTemplate loads a config without resolving the variables
    46  func LoadTemplate(configDict map[string]interface{}) (*templatetypes.Config, error) {
    47  	if err := validateForbidden(configDict); err != nil {
    48  		return nil, err
    49  	}
    50  	return loadSections(configDict, types.ConfigDetails{})
    51  }
    52  
    53  func validateForbidden(configDict map[string]interface{}) error {
    54  	servicesDict, ok := configDict["services"].(map[string]interface{})
    55  	if !ok {
    56  		return nil
    57  	}
    58  	forbidden := getProperties(servicesDict, types.ForbiddenProperties)
    59  	if len(forbidden) > 0 {
    60  		return &ForbiddenPropertiesError{Properties: forbidden}
    61  	}
    62  	return nil
    63  }
    64  
    65  func loadSections(config map[string]interface{}, configDetails types.ConfigDetails) (*templatetypes.Config, error) {
    66  	var err error
    67  	cfg := templatetypes.Config{
    68  		Version: schema.Version(config),
    69  	}
    70  
    71  	var loaders = []struct {
    72  		key string
    73  		fnc func(config map[string]interface{}) error
    74  	}{
    75  		{
    76  			key: "services",
    77  			fnc: func(config map[string]interface{}) error {
    78  				cfg.Services, err = LoadServices(config, configDetails.WorkingDir, configDetails.LookupEnv)
    79  				return err
    80  			},
    81  		},
    82  		{
    83  			key: "networks",
    84  			fnc: func(config map[string]interface{}) error {
    85  				cfg.Networks, err = loader.LoadNetworks(config, configDetails.Version)
    86  				return err
    87  			},
    88  		},
    89  		{
    90  			key: "volumes",
    91  			fnc: func(config map[string]interface{}) error {
    92  				cfg.Volumes, err = loader.LoadVolumes(config, configDetails.Version)
    93  				return err
    94  			},
    95  		},
    96  		{
    97  			key: "secrets",
    98  			fnc: func(config map[string]interface{}) error {
    99  				cfg.Secrets, err = loader.LoadSecrets(config, configDetails)
   100  				return err
   101  			},
   102  		},
   103  		{
   104  			key: "configs",
   105  			fnc: func(config map[string]interface{}) error {
   106  				cfg.Configs, err = loader.LoadConfigObjs(config, configDetails)
   107  				return err
   108  			},
   109  		},
   110  	}
   111  	for _, loader := range loaders {
   112  		if err := loader.fnc(getSection(config, loader.key)); err != nil {
   113  			return nil, err
   114  		}
   115  	}
   116  	return &cfg, nil
   117  }
   118  
   119  func getSection(config map[string]interface{}, key string) map[string]interface{} {
   120  	section, ok := config[key]
   121  	if !ok {
   122  		return make(map[string]interface{})
   123  	}
   124  	return section.(map[string]interface{})
   125  }
   126  
   127  // GetUnsupportedProperties returns the list of any unsupported properties that are
   128  // used in the Compose files.
   129  func GetUnsupportedProperties(configDicts ...map[string]interface{}) []string {
   130  	unsupported := map[string]bool{}
   131  
   132  	for _, configDict := range configDicts {
   133  		for _, service := range getServices(configDict) {
   134  			serviceDict := service.(map[string]interface{})
   135  			for _, property := range types.UnsupportedProperties {
   136  				if _, isSet := serviceDict[property]; isSet {
   137  					unsupported[property] = true
   138  				}
   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(configDicts ...map[string]interface{}) map[string]string {
   158  	deprecated := map[string]string{}
   159  
   160  	for _, configDict := range configDicts {
   161  		deprecatedProperties := getProperties(getServices(configDict), types.DeprecatedProperties)
   162  		for key, value := range deprecatedProperties {
   163  			deprecated[key] = value
   164  		}
   165  	}
   166  
   167  	return deprecated
   168  }
   169  
   170  func getProperties(services map[string]interface{}, propertyMap map[string]string) map[string]string {
   171  	output := map[string]string{}
   172  
   173  	for _, service := range services {
   174  		if serviceDict, ok := service.(map[string]interface{}); ok {
   175  			for property, description := range propertyMap {
   176  				if _, isSet := serviceDict[property]; isSet {
   177  					output[property] = description
   178  				}
   179  			}
   180  		}
   181  	}
   182  
   183  	return output
   184  }
   185  
   186  // ForbiddenPropertiesError is returned when there are properties in the Compose
   187  // file that are forbidden.
   188  type ForbiddenPropertiesError struct {
   189  	Properties map[string]string
   190  }
   191  
   192  func (e *ForbiddenPropertiesError) Error() string {
   193  	return "Configuration contains forbidden properties"
   194  }
   195  
   196  func getServices(configDict map[string]interface{}) map[string]interface{} {
   197  	if services, ok := configDict["services"]; ok {
   198  		if servicesDict, ok := services.(map[string]interface{}); ok {
   199  			return servicesDict
   200  		}
   201  	}
   202  
   203  	return map[string]interface{}{}
   204  }
   205  
   206  // LoadServices produces a ServiceConfig map from a compose file Dict
   207  // the servicesDict is not validated if directly used. Use Load() to enable validation
   208  func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) ([]templatetypes.ServiceConfig, error) {
   209  	var services []templatetypes.ServiceConfig
   210  
   211  	for name, serviceDef := range servicesDict {
   212  		serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, lookupEnv)
   213  		if err != nil {
   214  			return nil, err
   215  		}
   216  		services = append(services, *serviceConfig)
   217  	}
   218  
   219  	return services, nil
   220  }
   221  
   222  // LoadService produces a single ServiceConfig from a compose file Dict
   223  // the serviceDict is not validated if directly used. Use Load() to enable validation
   224  func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*templatetypes.ServiceConfig, error) {
   225  	serviceConfig := &templatetypes.ServiceConfig{}
   226  	if err := loader.Transform(serviceDict, serviceConfig, transformers...); err != nil {
   227  		return nil, err
   228  	}
   229  	serviceConfig.Name = name
   230  
   231  	if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil {
   232  		return nil, err
   233  	}
   234  
   235  	if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil {
   236  		return nil, err
   237  	}
   238  	return serviceConfig, nil
   239  }
   240  
   241  func updateEnvironmentMap(environment templatetypes.MappingWithEqualsTemplate, vars map[string]*string, lookupEnv template.Mapping) {
   242  	for k, v := range vars {
   243  		interpolatedV, ok := lookupEnv(k)
   244  		if (v == nil || *v == "") && ok {
   245  			// lookupEnv is prioritized over vars
   246  			environment[templatetypes.StringTemplate{Value:k}] = &templatetypes.StringTemplate{Value: interpolatedV}
   247  		} else if v == nil {
   248  			environment[templatetypes.StringTemplate{Value:k}] = nil
   249  		} else {
   250  			environment[templatetypes.StringTemplate{Value:k}] = &templatetypes.StringTemplate{Value: *v}
   251  		}
   252  	}
   253  }
   254  func updateEnvironmentMapTemplate(environment, vars templatetypes.MappingWithEqualsTemplate, lookupEnv template.Mapping) {
   255  	for k, v := range vars {
   256  		interpolatedV, ok := lookupEnv(k.Value)
   257  		if (v == nil || v.Value == "") && ok {
   258  			// lookupEnv is prioritized over vars
   259  			environment[k] = &templatetypes.StringTemplate{Value: interpolatedV}
   260  		} else {
   261  			environment[k] = v
   262  		}
   263  	}
   264  }
   265  
   266  
   267  func resolveEnvironment(serviceConfig *templatetypes.ServiceConfig, workingDir string, lookupEnv template.Mapping) error {
   268  	environment := templatetypes.MappingWithEqualsTemplate{}
   269  
   270  	if len(serviceConfig.EnvFile) > 0 {
   271  		var envVars []string
   272  
   273  		for _, file := range serviceConfig.EnvFile {
   274  			filePath := absPath(workingDir, file.Value)
   275  			fileVars, err := opts.ParseEnvFile(filePath)
   276  			if err != nil {
   277  				return err
   278  			}
   279  			envVars = append(envVars, fileVars...)
   280  		}
   281  		updateEnvironmentMap(environment,
   282  			opts.ConvertKVStringsToMapWithNil(envVars), lookupEnv)
   283  	}
   284  
   285  	updateEnvironmentMapTemplate(environment, serviceConfig.Environment, lookupEnv)
   286  	serviceConfig.Environment = environment
   287  	return nil
   288  }
   289  
   290  func resolveVolumePaths(volumes []templatetypes.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error {
   291  	for i, volume := range volumes {
   292  		if volume.Type != "bind" {
   293  			continue
   294  		}
   295  
   296  		if volume.Source.Value == "" {
   297  			return errors.New(`invalid mount config for type "bind": field Source must not be empty`)
   298  		}
   299  
   300  		filePath := expandUser(volume.Source.Value, lookupEnv)
   301  		// Check for a Unix absolute path first, to handle a Windows client
   302  		// with a Unix daemon. This handles a Windows client connecting to a
   303  		// Unix daemon. Note that this is not required for Docker for Windows
   304  		// when specifying a local Windows path, because Docker for Windows
   305  		// translates the Windows path into a valid path within the VM.
   306  		if !path.IsAbs(filePath) {
   307  			filePath = absPath(workingDir, filePath)
   308  		}
   309  		volume.Source.Value = filePath
   310  		volumes[i] = volume
   311  	}
   312  	return nil
   313  }
   314  
   315  // TODO: make this more robust
   316  func expandUser(path string, lookupEnv template.Mapping) string {
   317  	if strings.HasPrefix(path, "~") {
   318  		home, ok := lookupEnv("HOME")
   319  		if !ok {
   320  			logrus.Warn("cannot expand '~', because the environment lacks HOME")
   321  			return path
   322  		}
   323  		return strings.Replace(path, "~", home, 1)
   324  	}
   325  	return path
   326  }
   327  
   328  func absPath(workingDir string, filePath string) string {
   329  	if filepath.IsAbs(filePath) {
   330  		return filePath
   331  	}
   332  	return filepath.Join(workingDir, filePath)
   333  }
   334  
   335  func transformServicePort(data interface{}) (interface{}, error) {
   336  	switch entries := data.(type) {
   337  	case []interface{}:
   338  		// We process the list instead of individual items here.
   339  		// The reason is that one entry might be mapped to multiple ServicePortConfig.
   340  		// Therefore we take an input of a list and return an output of a list.
   341  		ports := []interface{}{}
   342  		for _, entry := range entries {
   343  			switch value := entry.(type) {
   344  			case int:
   345  				v, err := toServicePortConfigs(fmt.Sprint(value))
   346  				if err != nil {
   347  					return data, err
   348  				}
   349  				ports = append(ports, v...)
   350  			case string:
   351  				v, err := toServicePortConfigs(value)
   352  				if err != nil {
   353  					return data, err
   354  				}
   355  				ports = append(ports, v...)
   356  			case map[string]interface{}:
   357  				ports = append(ports, value)
   358  			default:
   359  				return data, errors.Errorf("invalid type %T for port", value)
   360  			}
   361  		}
   362  		return ports, nil
   363  	default:
   364  		return data, errors.Errorf("invalid type %T for port", entries)
   365  	}
   366  }
   367  
   368  func transformStringSourceMap(data interface{}) (interface{}, error) {
   369  	switch value := data.(type) {
   370  	case string:
   371  		return map[string]interface{}{"source": value}, nil
   372  	case map[string]interface{}:
   373  		return data, nil
   374  	default:
   375  		return data, errors.Errorf("invalid type %T for secret", value)
   376  	}
   377  }
   378  
   379  func transformServiceVolumeConfig(data interface{}) (interface{}, error) {
   380  	switch value := data.(type) {
   381  	case string:
   382  		return ParseVolume(value)
   383  	case map[string]interface{}:
   384  		return data, nil
   385  	default:
   386  		return data, errors.Errorf("invalid type %T for service volume", value)
   387  	}
   388  }
   389  
   390  func transformBoolOrTemplate(value interface{}) (interface{}, error) {
   391  	switch value := value.(type) {
   392  	case int:
   393  		return templatetypes.BoolOrTemplate{Value: value != 0}, nil
   394  	case bool:
   395  		return templatetypes.BoolOrTemplate{Value: value}, nil
   396  	case string:
   397  		b, err := toBoolean(value)
   398  		if err == nil {
   399  			return templatetypes.BoolOrTemplate{Value: b.(bool)}, nil
   400  		}
   401  		return templatetypes.BoolOrTemplate{ValueTemplate: value}, nil
   402  	default:
   403  		return value, errors.Errorf("invali type %T for boolean", value)
   404  	}
   405  }
   406  
   407  func transformUInt64OrTemplate(value interface{}) (interface{}, error) {
   408  	switch value := value.(type) {
   409  	case int:
   410  		v := uint64(value)
   411  		return templatetypes.UInt64OrTemplate{Value: &v}, nil
   412  	case string:
   413  		v, err := strconv.ParseUint(value, 0, 64)
   414  		if err == nil {
   415  			return templatetypes.UInt64OrTemplate{Value: &v}, nil
   416  		}
   417  		return templatetypes.UInt64OrTemplate{ValueTemplate: value}, nil
   418  	default:
   419  		return value, errors.Errorf("invali type %T for boolean", value)
   420  	}
   421  }
   422  
   423  func transformDurationOrTemplate(value interface{}) (interface{}, error) {
   424  	switch value := value.(type) {
   425  	case int:
   426  		d := time.Duration(value)
   427  		return templatetypes.DurationOrTemplate{Value: &d}, nil
   428  	case string:
   429  		d, err := time.ParseDuration(value)
   430  		if err == nil {
   431  			return templatetypes.DurationOrTemplate{Value: &d}, nil
   432  		}
   433  		return templatetypes.DurationOrTemplate{ValueTemplate: value}, nil
   434  	default:
   435  		return nil, errors.Errorf("invalid type for duration %T", value)
   436  	}
   437  }
   438  
   439  func transformSize(value interface{}) (interface{}, error) {
   440  	switch value := value.(type) {
   441  	case int:
   442  		return templatetypes.UnitBytesOrTemplate{Value: int64(value)}, nil
   443  	case string:
   444  		v, err := units.RAMInBytes(value)
   445  		if err == nil {
   446  			return templatetypes.UnitBytesOrTemplate{Value: int64(v)}, nil
   447  		}
   448  		return templatetypes.UnitBytesOrTemplate{ValueTemplate: value}, nil
   449  	}
   450  	return nil, errors.Errorf("invalid type for size %T", value)
   451  }
   452  
   453  func transformStringTemplate(value interface{}) (interface{}, error) {
   454  	return templatetypes.StringTemplate{Value: fmt.Sprintf("%v", value)}, nil
   455  }
   456  
   457  func transformStringTemplateList(data interface{}) (interface{}, error) {
   458  	switch value := data.(type) {
   459  	case string:
   460  		return templatetypes.StringTemplateList{templatetypes.StringTemplate{Value: value}}, nil
   461  	case []interface{}:
   462  		res := templatetypes.StringTemplateList{}
   463  		for _, v := range value {
   464  			res = append(res, templatetypes.StringTemplate{ Value: fmt.Sprintf("%v", v)})
   465  		}
   466  		return res, nil
   467  	default:
   468  		return data, errors.Errorf("invalid type %T for string list", value)
   469  	}
   470  }
   471  
   472  func transformShellCommandTemplate(value interface{}) (interface{}, error) {
   473  	if str, ok := value.(string); ok {
   474  		return shellwords.Parse(str)
   475  	}
   476  	return value, nil
   477  }
   478  
   479  func transformHostsListTemplate(data interface{}) (interface{}, error) {
   480  	return transformListOrMapping(data, ":", false), nil
   481  }
   482  
   483  func toStringList(value map[string]interface{}, separator string, allowNil bool) []string {
   484  	output := []string{}
   485  	for key, value := range value {
   486  		if value == nil && !allowNil {
   487  			continue
   488  		}
   489  		output = append(output, fmt.Sprintf("%s%s%s", key, separator, value))
   490  	}
   491  	sort.Strings(output)
   492  	return output
   493  }
   494  
   495  func toString(value interface{}, allowNil bool) interface{} {
   496  	switch {
   497  	case value != nil:
   498  		return fmt.Sprint(value)
   499  	case allowNil:
   500  		return nil
   501  	default:
   502  		return ""
   503  	}
   504  }
   505  
   506  func toMapStringString(value map[string]interface{}, allowNil bool) map[string]interface{} {
   507  	output := make(map[string]interface{})
   508  	for key, value := range value {
   509  		output[key] = toString(value, allowNil)
   510  	}
   511  	return output
   512  }
   513  
   514  func transformListOrMapping(listOrMapping interface{}, sep string, allowNil bool) interface{} {
   515  	switch value := listOrMapping.(type) {
   516  	case map[string]interface{}:
   517  		return toStringList(value, sep, allowNil)
   518  	case []interface{}:
   519  		return listOrMapping
   520  	}
   521  	panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping))
   522  }
   523  
   524  func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} {
   525  	switch value := mappingOrList.(type) {
   526  	case map[string]interface{}:
   527  		return toMapStringString(value, allowNil)
   528  	case ([]interface{}):
   529  		result := make(map[string]interface{})
   530  		for _, value := range value {
   531  			parts := strings.SplitN(value.(string), sep, 2)
   532  			key := parts[0]
   533  			switch {
   534  			case len(parts) == 1 && allowNil:
   535  				result[key] = nil
   536  			case len(parts) == 1 && !allowNil:
   537  				result[key] = ""
   538  			default:
   539  				result[key] = parts[1]
   540  			}
   541  		}
   542  		return result
   543  	}
   544  	panic(errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList))
   545  }
   546  
   547  func transformMappingOrListFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) {
   548  	return func(data interface{}) (interface{}, error) {
   549  		return transformMappingOrList(data, sep, allowNil), nil
   550  	}
   551  }
   552  
   553  func toServicePortConfigs(value string) ([]interface{}, error) {
   554  	var portConfigs []interface{}
   555  	if strings.Contains(value, "$") {
   556  		// template detected
   557  		if strings.Contains(value, "-") {
   558  			return nil, fmt.Errorf("port range not supported with templated values")
   559  		}
   560  		portsProtocol := strings.Split(value, "/")
   561  		protocol := "tcp"
   562  		if len(portsProtocol) > 1 {
   563  			protocol = portsProtocol[1]
   564  		}
   565  		portPort := strings.Split(portsProtocol[0], ":")
   566  		tgt, _ := transformUInt64OrTemplate(portPort[0]) // can't fail on string
   567  		pub := templatetypes.UInt64OrTemplate{}
   568  		if len(portPort) > 1 {
   569  			ipub, _ := transformUInt64OrTemplate(portPort[1])
   570  			pub = ipub.(templatetypes.UInt64OrTemplate)
   571  		}
   572  		portConfigs = append(portConfigs, templatetypes.ServicePortConfig{
   573  			Protocol:  templatetypes.StringTemplate{Value: protocol},
   574  			Target:    tgt.(templatetypes.UInt64OrTemplate),
   575  			Published: pub,
   576  			Mode:      templatetypes.StringTemplate{Value: "ingress"},
   577  		})
   578  		return portConfigs, nil
   579  	}
   580  
   581  	ports, portBindings, err := nat.ParsePortSpecs([]string{value})
   582  	if err != nil {
   583  		return nil, err
   584  	}
   585  	// We need to sort the key of the ports to make sure it is consistent
   586  	keys := []string{}
   587  	for port := range ports {
   588  		keys = append(keys, string(port))
   589  	}
   590  	sort.Strings(keys)
   591  
   592  	for _, key := range keys {
   593  		// Reuse ConvertPortToPortConfig so that it is consistent
   594  		portConfig, err := opts.ConvertPortToPortConfig(nat.Port(key), portBindings)
   595  		if err != nil {
   596  			return nil, err
   597  		}
   598  		for _, p := range portConfig {
   599  			tp := uint64(p.TargetPort)
   600  			pp := uint64(p.PublishedPort)
   601  			portConfigs = append(portConfigs, templatetypes.ServicePortConfig{
   602  				Protocol:  templatetypes.StringTemplate{Value: string(p.Protocol)},
   603  				Target:    templatetypes.UInt64OrTemplate{Value: &tp},
   604  				Published: templatetypes.UInt64OrTemplate{Value: &pp},
   605  				Mode:      templatetypes.StringTemplate{Value: string(p.PublishMode)},
   606  			})
   607  		}
   608  	}
   609  
   610  	return portConfigs, nil
   611  }