github.com/elopio/cli@v6.21.2-0.20160902224010-ea909d1fdb2f+incompatible/cf/manifest/manifest.go (about)

     1  package manifest
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"path/filepath"
     7  	"regexp"
     8  	"strconv"
     9  	"strings"
    10  
    11  	. "code.cloudfoundry.org/cli/cf/i18n"
    12  
    13  	"code.cloudfoundry.org/cli/cf/formatters"
    14  	"code.cloudfoundry.org/cli/cf/models"
    15  	"code.cloudfoundry.org/cli/utils/generic"
    16  	"code.cloudfoundry.org/cli/utils/words/generator"
    17  )
    18  
    19  type Manifest struct {
    20  	Path string
    21  	Data generic.Map
    22  }
    23  
    24  func NewEmptyManifest() (m *Manifest) {
    25  	return &Manifest{Data: generic.NewMap()}
    26  }
    27  
    28  func (m Manifest) Applications() ([]models.AppParams, error) {
    29  	rawData, err := expandProperties(m.Data, generator.NewWordGenerator())
    30  	if err != nil {
    31  		return []models.AppParams{}, err
    32  	}
    33  
    34  	data := generic.NewMap(rawData)
    35  	appMaps, err := m.getAppMaps(data)
    36  	if err != nil {
    37  		return []models.AppParams{}, err
    38  	}
    39  
    40  	var apps []models.AppParams
    41  	var mapToAppErrs []error
    42  	for _, appMap := range appMaps {
    43  		app, err := mapToAppParams(filepath.Dir(m.Path), appMap)
    44  		if err != nil {
    45  			mapToAppErrs = append(mapToAppErrs, err)
    46  			continue
    47  		}
    48  
    49  		apps = append(apps, app)
    50  	}
    51  
    52  	if len(mapToAppErrs) > 0 {
    53  		message := ""
    54  		for i := range mapToAppErrs {
    55  			message = message + fmt.Sprintf("%s\n", mapToAppErrs[i].Error())
    56  		}
    57  		return []models.AppParams{}, errors.New(message)
    58  	}
    59  
    60  	return apps, nil
    61  }
    62  
    63  func (m Manifest) getAppMaps(data generic.Map) ([]generic.Map, error) {
    64  	globalProperties := data.Except([]interface{}{"applications"})
    65  
    66  	var apps []generic.Map
    67  	var errs []error
    68  	if data.Has("applications") {
    69  		appMaps, ok := data.Get("applications").([]interface{})
    70  		if !ok {
    71  			return []generic.Map{}, errors.New(T("Expected applications to be a list"))
    72  		}
    73  
    74  		for _, appData := range appMaps {
    75  			if !generic.IsMappable(appData) {
    76  				errs = append(errs, fmt.Errorf(T("Expected application to be a list of key/value pairs\nError occurred in manifest near:\n'{{.YmlSnippet}}'",
    77  					map[string]interface{}{"YmlSnippet": appData})))
    78  				continue
    79  			}
    80  
    81  			appMap := generic.DeepMerge(globalProperties, generic.NewMap(appData))
    82  			apps = append(apps, appMap)
    83  		}
    84  	} else {
    85  		apps = append(apps, globalProperties)
    86  	}
    87  
    88  	if len(errs) > 0 {
    89  		message := ""
    90  		for i := range errs {
    91  			message = message + fmt.Sprintf("%s\n", errs[i].Error())
    92  		}
    93  		return []generic.Map{}, errors.New(message)
    94  	}
    95  
    96  	return apps, nil
    97  }
    98  
    99  var propertyRegex = regexp.MustCompile(`\${[\w-]+}`)
   100  
   101  func expandProperties(input interface{}, babbler generator.WordGenerator) (interface{}, error) {
   102  	var errs []error
   103  	var output interface{}
   104  
   105  	switch input := input.(type) {
   106  	case string:
   107  		match := propertyRegex.FindStringSubmatch(input)
   108  		if match != nil {
   109  			if match[0] == "${random-word}" {
   110  				output = strings.Replace(input, "${random-word}", strings.ToLower(babbler.Babble()), -1)
   111  			} else {
   112  				err := fmt.Errorf(T("Property '{{.PropertyName}}' found in manifest. This feature is no longer supported. Please remove it and try again.",
   113  					map[string]interface{}{"PropertyName": match[0]}))
   114  				errs = append(errs, err)
   115  			}
   116  		} else {
   117  			output = input
   118  		}
   119  	case []interface{}:
   120  		outputSlice := make([]interface{}, len(input))
   121  		for index, item := range input {
   122  			itemOutput, itemErr := expandProperties(item, babbler)
   123  			if itemErr != nil {
   124  				errs = append(errs, itemErr)
   125  				break
   126  			}
   127  			outputSlice[index] = itemOutput
   128  		}
   129  		output = outputSlice
   130  	case map[interface{}]interface{}:
   131  		outputMap := make(map[interface{}]interface{})
   132  		for key, value := range input {
   133  			itemOutput, itemErr := expandProperties(value, babbler)
   134  			if itemErr != nil {
   135  				errs = append(errs, itemErr)
   136  				break
   137  			}
   138  			outputMap[key] = itemOutput
   139  		}
   140  		output = outputMap
   141  	case generic.Map:
   142  		outputMap := generic.NewMap()
   143  		generic.Each(input, func(key, value interface{}) {
   144  			itemOutput, itemErr := expandProperties(value, babbler)
   145  			if itemErr != nil {
   146  				errs = append(errs, itemErr)
   147  				return
   148  			}
   149  			outputMap.Set(key, itemOutput)
   150  		})
   151  		output = outputMap
   152  	default:
   153  		output = input
   154  	}
   155  
   156  	if len(errs) > 0 {
   157  		message := ""
   158  		for _, err := range errs {
   159  			message = message + fmt.Sprintf("%s\n", err.Error())
   160  		}
   161  		return nil, errors.New(message)
   162  	}
   163  
   164  	return output, nil
   165  }
   166  
   167  func mapToAppParams(basePath string, yamlMap generic.Map) (models.AppParams, error) {
   168  	err := checkForNulls(yamlMap)
   169  	if err != nil {
   170  		return models.AppParams{}, err
   171  	}
   172  
   173  	var appParams models.AppParams
   174  	var errs []error
   175  	appParams.BuildpackURL = stringValOrDefault(yamlMap, "buildpack", &errs)
   176  	appParams.DiskQuota = bytesVal(yamlMap, "disk_quota", &errs)
   177  
   178  	domainAry := sliceOrNil(yamlMap, "domains", &errs)
   179  	if domain := stringVal(yamlMap, "domain", &errs); domain != nil {
   180  		if domainAry == nil {
   181  			domainAry = []string{*domain}
   182  		} else {
   183  			domainAry = append(domainAry, *domain)
   184  		}
   185  	}
   186  	appParams.Domains = removeDuplicatedValue(domainAry)
   187  
   188  	hostsArr := sliceOrNil(yamlMap, "hosts", &errs)
   189  	if host := stringVal(yamlMap, "host", &errs); host != nil {
   190  		hostsArr = append(hostsArr, *host)
   191  	}
   192  	appParams.Hosts = removeDuplicatedValue(hostsArr)
   193  
   194  	appParams.Name = stringVal(yamlMap, "name", &errs)
   195  	appParams.Path = stringVal(yamlMap, "path", &errs)
   196  	appParams.StackName = stringVal(yamlMap, "stack", &errs)
   197  	appParams.Command = stringValOrDefault(yamlMap, "command", &errs)
   198  	appParams.Memory = bytesVal(yamlMap, "memory", &errs)
   199  	appParams.InstanceCount = intVal(yamlMap, "instances", &errs)
   200  	appParams.HealthCheckTimeout = intVal(yamlMap, "timeout", &errs)
   201  	appParams.NoRoute = boolVal(yamlMap, "no-route", &errs)
   202  	appParams.NoHostname = boolOrNil(yamlMap, "no-hostname", &errs)
   203  	appParams.UseRandomRoute = boolVal(yamlMap, "random-route", &errs)
   204  	appParams.ServicesToBind = sliceOrNil(yamlMap, "services", &errs)
   205  	appParams.EnvironmentVars = envVarOrEmptyMap(yamlMap, &errs)
   206  	appParams.HealthCheckType = stringVal(yamlMap, "health-check-type", &errs)
   207  	appParams.AppPorts = intSliceVal(yamlMap, "app-ports", &errs)
   208  	appParams.Routes = parseRoutes(yamlMap, &errs)
   209  
   210  	if appParams.Path != nil {
   211  		path := *appParams.Path
   212  		if filepath.IsAbs(path) {
   213  			path = filepath.Clean(path)
   214  		} else {
   215  			path = filepath.Join(basePath, path)
   216  		}
   217  		appParams.Path = &path
   218  	}
   219  
   220  	if len(errs) > 0 {
   221  		message := ""
   222  		for _, err := range errs {
   223  			message = message + fmt.Sprintf("%s\n", err.Error())
   224  		}
   225  		return models.AppParams{}, errors.New(message)
   226  	}
   227  
   228  	return appParams, nil
   229  }
   230  
   231  func removeDuplicatedValue(ary []string) []string {
   232  	if ary == nil {
   233  		return nil
   234  	}
   235  
   236  	m := make(map[string]bool)
   237  	for _, v := range ary {
   238  		m[v] = true
   239  	}
   240  
   241  	newAry := []string{}
   242  	for _, val := range ary {
   243  		if m[val] {
   244  			newAry = append(newAry, val)
   245  			m[val] = false
   246  		}
   247  	}
   248  	return newAry
   249  }
   250  
   251  func checkForNulls(yamlMap generic.Map) error {
   252  	var errs []error
   253  	generic.Each(yamlMap, func(key interface{}, value interface{}) {
   254  		if key == "command" || key == "buildpack" {
   255  			return
   256  		}
   257  		if value == nil {
   258  			errs = append(errs, fmt.Errorf(T("{{.PropertyName}} should not be null", map[string]interface{}{"PropertyName": key})))
   259  		}
   260  	})
   261  
   262  	if len(errs) > 0 {
   263  		message := ""
   264  		for i := range errs {
   265  			message = message + fmt.Sprintf("%s\n", errs[i].Error())
   266  		}
   267  		return errors.New(message)
   268  	}
   269  
   270  	return nil
   271  }
   272  
   273  func stringVal(yamlMap generic.Map, key string, errs *[]error) *string {
   274  	val := yamlMap.Get(key)
   275  	if val == nil {
   276  		return nil
   277  	}
   278  	result, ok := val.(string)
   279  	if !ok {
   280  		*errs = append(*errs, fmt.Errorf(T("{{.PropertyName}} must be a string value", map[string]interface{}{"PropertyName": key})))
   281  		return nil
   282  	}
   283  	return &result
   284  }
   285  
   286  func stringValOrDefault(yamlMap generic.Map, key string, errs *[]error) *string {
   287  	if !yamlMap.Has(key) {
   288  		return nil
   289  	}
   290  	empty := ""
   291  	switch val := yamlMap.Get(key).(type) {
   292  	case string:
   293  		if val == "default" {
   294  			return &empty
   295  		}
   296  		return &val
   297  	case nil:
   298  		return &empty
   299  	default:
   300  		*errs = append(*errs, fmt.Errorf(T("{{.PropertyName}} must be a string or null value", map[string]interface{}{"PropertyName": key})))
   301  		return nil
   302  	}
   303  }
   304  
   305  func bytesVal(yamlMap generic.Map, key string, errs *[]error) *int64 {
   306  	yamlVal := yamlMap.Get(key)
   307  	if yamlVal == nil {
   308  		return nil
   309  	}
   310  
   311  	stringVal := coerceToString(yamlVal)
   312  	value, err := formatters.ToMegabytes(stringVal)
   313  	if err != nil {
   314  		*errs = append(*errs, fmt.Errorf(T("Invalid value for '{{.PropertyName}}': {{.StringVal}}\n{{.Error}}",
   315  			map[string]interface{}{
   316  				"PropertyName": key,
   317  				"Error":        err.Error(),
   318  				"StringVal":    stringVal,
   319  			})))
   320  		return nil
   321  	}
   322  	return &value
   323  }
   324  
   325  func intVal(yamlMap generic.Map, key string, errs *[]error) *int {
   326  	var (
   327  		intVal int
   328  		err    error
   329  	)
   330  
   331  	switch val := yamlMap.Get(key).(type) {
   332  	case string:
   333  		intVal, err = strconv.Atoi(val)
   334  	case int:
   335  		intVal = val
   336  	case int64:
   337  		intVal = int(val)
   338  	case nil:
   339  		return nil
   340  	default:
   341  		err = fmt.Errorf(T("Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.",
   342  			map[string]interface{}{"PropertyName": key, "PropertyType": val}))
   343  	}
   344  
   345  	if err != nil {
   346  		*errs = append(*errs, err)
   347  		return nil
   348  	}
   349  
   350  	return &intVal
   351  }
   352  
   353  func coerceToString(value interface{}) string {
   354  	return fmt.Sprintf("%v", value)
   355  }
   356  
   357  func boolVal(yamlMap generic.Map, key string, errs *[]error) bool {
   358  	switch val := yamlMap.Get(key).(type) {
   359  	case nil:
   360  		return false
   361  	case bool:
   362  		return val
   363  	case string:
   364  		return val == "true"
   365  	default:
   366  		*errs = append(*errs, fmt.Errorf(T("Expected {{.PropertyName}} to be a boolean.", map[string]interface{}{"PropertyName": key})))
   367  		return false
   368  	}
   369  }
   370  
   371  func boolOrNil(yamlMap generic.Map, key string, errs *[]error) *bool {
   372  	result := false
   373  	switch val := yamlMap.Get(key).(type) {
   374  	case nil:
   375  		return nil
   376  	case bool:
   377  		return &val
   378  	case string:
   379  		result = val == "true"
   380  		return &result
   381  	default:
   382  		*errs = append(*errs, fmt.Errorf(T("Expected {{.PropertyName}} to be a boolean.", map[string]interface{}{"PropertyName": key})))
   383  		return &result
   384  	}
   385  }
   386  func sliceOrNil(yamlMap generic.Map, key string, errs *[]error) []string {
   387  	if !yamlMap.Has(key) {
   388  		return nil
   389  	}
   390  
   391  	var err error
   392  	stringSlice := []string{}
   393  
   394  	sliceErr := fmt.Errorf(T("Expected {{.PropertyName}} to be a list of strings.", map[string]interface{}{"PropertyName": key}))
   395  
   396  	switch input := yamlMap.Get(key).(type) {
   397  	case []interface{}:
   398  		for _, value := range input {
   399  			stringValue, ok := value.(string)
   400  			if !ok {
   401  				err = sliceErr
   402  				break
   403  			}
   404  			stringSlice = append(stringSlice, stringValue)
   405  		}
   406  	default:
   407  		err = sliceErr
   408  	}
   409  
   410  	if err != nil {
   411  		*errs = append(*errs, err)
   412  		return []string{}
   413  	}
   414  
   415  	return stringSlice
   416  }
   417  
   418  func intSliceVal(yamlMap generic.Map, key string, errs *[]error) *[]int {
   419  	if !yamlMap.Has(key) {
   420  		return nil
   421  	}
   422  
   423  	err := fmt.Errorf(T("Expected {{.PropertyName}} to be a list of integers.", map[string]interface{}{"PropertyName": key}))
   424  
   425  	s, ok := yamlMap.Get(key).([]interface{})
   426  
   427  	if !ok {
   428  		*errs = append(*errs, err)
   429  		return nil
   430  	}
   431  
   432  	var intSlice []int
   433  
   434  	for _, el := range s {
   435  		intValue, ok := el.(int)
   436  
   437  		if !ok {
   438  			*errs = append(*errs, err)
   439  			return nil
   440  		}
   441  
   442  		intSlice = append(intSlice, intValue)
   443  	}
   444  
   445  	return &intSlice
   446  }
   447  
   448  func envVarOrEmptyMap(yamlMap generic.Map, errs *[]error) *map[string]interface{} {
   449  	key := "env"
   450  	switch envVars := yamlMap.Get(key).(type) {
   451  	case nil:
   452  		aMap := make(map[string]interface{}, 0)
   453  		return &aMap
   454  	case map[string]interface{}:
   455  		yamlMap.Set(key, generic.NewMap(yamlMap.Get(key)))
   456  		return envVarOrEmptyMap(yamlMap, errs)
   457  	case map[interface{}]interface{}:
   458  		yamlMap.Set(key, generic.NewMap(yamlMap.Get(key)))
   459  		return envVarOrEmptyMap(yamlMap, errs)
   460  	case generic.Map:
   461  		merrs := validateEnvVars(envVars)
   462  		if merrs != nil {
   463  			*errs = append(*errs, merrs...)
   464  			return nil
   465  		}
   466  
   467  		result := make(map[string]interface{}, envVars.Count())
   468  		generic.Each(envVars, func(key, value interface{}) {
   469  			result[key.(string)] = value
   470  		})
   471  
   472  		return &result
   473  	default:
   474  		*errs = append(*errs, fmt.Errorf(T("Expected {{.Name}} to be a set of key => value, but it was a {{.Type}}.",
   475  			map[string]interface{}{"Name": key, "Type": envVars})))
   476  		return nil
   477  	}
   478  }
   479  
   480  func validateEnvVars(input generic.Map) (errs []error) {
   481  	generic.Each(input, func(key, value interface{}) {
   482  		if value == nil {
   483  			errs = append(errs, fmt.Errorf(T("env var '{{.PropertyName}}' should not be null",
   484  				map[string]interface{}{"PropertyName": key})))
   485  		}
   486  	})
   487  	return
   488  }
   489  
   490  func parseRoutes(input generic.Map, errs *[]error) []models.ManifestRoute {
   491  	if !input.Has("routes") {
   492  		return nil
   493  	}
   494  
   495  	genericRoutes, ok := input.Get("routes").([]interface{})
   496  	if !ok {
   497  		*errs = append(*errs, fmt.Errorf(T("'routes' should be a list")))
   498  		return nil
   499  	}
   500  
   501  	manifestRoutes := []models.ManifestRoute{}
   502  	for _, genericRoute := range genericRoutes {
   503  		route, ok := genericRoute.(map[interface{}]interface{})
   504  		if !ok {
   505  			*errs = append(*errs, fmt.Errorf(T("each route in 'routes' must have a 'route' property")))
   506  			continue
   507  		}
   508  
   509  		if routeVal, exist := route["route"]; exist {
   510  			manifestRoutes = append(manifestRoutes, models.ManifestRoute{
   511  				Route: routeVal.(string),
   512  			})
   513  		} else {
   514  			*errs = append(*errs, fmt.Errorf(T("each route in 'routes' must have a 'route' property")))
   515  		}
   516  	}
   517  
   518  	return manifestRoutes
   519  }