github.com/dcarley/cf-cli@v6.24.1-0.20170220111324-4225ff346898+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/util/generic"
    16  	"code.cloudfoundry.org/cli/util/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.HealthCheckHTTPEndpoint = stringVal(yamlMap, "health-check-http-endpoint", &errs)
   208  
   209  	appParams.AppPorts = intSliceVal(yamlMap, "app-ports", &errs)
   210  	appParams.Routes = parseRoutes(yamlMap, &errs)
   211  
   212  	if appParams.Path != nil {
   213  		path := *appParams.Path
   214  		if filepath.IsAbs(path) {
   215  			path = filepath.Clean(path)
   216  		} else {
   217  			path = filepath.Join(basePath, path)
   218  		}
   219  		appParams.Path = &path
   220  	}
   221  
   222  	if len(errs) > 0 {
   223  		message := ""
   224  		for _, err := range errs {
   225  			message = message + fmt.Sprintf("%s\n", err.Error())
   226  		}
   227  		return models.AppParams{}, errors.New(message)
   228  	}
   229  
   230  	return appParams, nil
   231  }
   232  
   233  func removeDuplicatedValue(ary []string) []string {
   234  	if ary == nil {
   235  		return nil
   236  	}
   237  
   238  	m := make(map[string]bool)
   239  	for _, v := range ary {
   240  		m[v] = true
   241  	}
   242  
   243  	newAry := []string{}
   244  	for _, val := range ary {
   245  		if m[val] {
   246  			newAry = append(newAry, val)
   247  			m[val] = false
   248  		}
   249  	}
   250  	return newAry
   251  }
   252  
   253  func checkForNulls(yamlMap generic.Map) error {
   254  	var errs []error
   255  	generic.Each(yamlMap, func(key interface{}, value interface{}) {
   256  		if key == "command" || key == "buildpack" {
   257  			return
   258  		}
   259  		if value == nil {
   260  			errs = append(errs, fmt.Errorf(T("{{.PropertyName}} should not be null", map[string]interface{}{"PropertyName": key})))
   261  		}
   262  	})
   263  
   264  	if len(errs) > 0 {
   265  		message := ""
   266  		for i := range errs {
   267  			message = message + fmt.Sprintf("%s\n", errs[i].Error())
   268  		}
   269  		return errors.New(message)
   270  	}
   271  
   272  	return nil
   273  }
   274  
   275  func stringVal(yamlMap generic.Map, key string, errs *[]error) *string {
   276  	val := yamlMap.Get(key)
   277  	if val == nil {
   278  		return nil
   279  	}
   280  	result, ok := val.(string)
   281  	if !ok {
   282  		*errs = append(*errs, fmt.Errorf(T("{{.PropertyName}} must be a string value", map[string]interface{}{"PropertyName": key})))
   283  		return nil
   284  	}
   285  	return &result
   286  }
   287  
   288  func stringValOrDefault(yamlMap generic.Map, key string, errs *[]error) *string {
   289  	if !yamlMap.Has(key) {
   290  		return nil
   291  	}
   292  	empty := ""
   293  	switch val := yamlMap.Get(key).(type) {
   294  	case string:
   295  		if val == "default" {
   296  			return &empty
   297  		}
   298  		return &val
   299  	case nil:
   300  		return &empty
   301  	default:
   302  		*errs = append(*errs, fmt.Errorf(T("{{.PropertyName}} must be a string or null value", map[string]interface{}{"PropertyName": key})))
   303  		return nil
   304  	}
   305  }
   306  
   307  func bytesVal(yamlMap generic.Map, key string, errs *[]error) *int64 {
   308  	yamlVal := yamlMap.Get(key)
   309  	if yamlVal == nil {
   310  		return nil
   311  	}
   312  
   313  	stringVal := coerceToString(yamlVal)
   314  	value, err := formatters.ToMegabytes(stringVal)
   315  	if err != nil {
   316  		*errs = append(*errs, fmt.Errorf(T("Invalid value for '{{.PropertyName}}': {{.StringVal}}\n{{.Error}}",
   317  			map[string]interface{}{
   318  				"PropertyName": key,
   319  				"Error":        err.Error(),
   320  				"StringVal":    stringVal,
   321  			})))
   322  		return nil
   323  	}
   324  	return &value
   325  }
   326  
   327  func intVal(yamlMap generic.Map, key string, errs *[]error) *int {
   328  	var (
   329  		intVal int
   330  		err    error
   331  	)
   332  
   333  	switch val := yamlMap.Get(key).(type) {
   334  	case string:
   335  		intVal, err = strconv.Atoi(val)
   336  	case int:
   337  		intVal = val
   338  	case int64:
   339  		intVal = int(val)
   340  	case nil:
   341  		return nil
   342  	default:
   343  		err = fmt.Errorf(T("Expected {{.PropertyName}} to be a number, but it was a {{.PropertyType}}.",
   344  			map[string]interface{}{"PropertyName": key, "PropertyType": val}))
   345  	}
   346  
   347  	if err != nil {
   348  		*errs = append(*errs, err)
   349  		return nil
   350  	}
   351  
   352  	return &intVal
   353  }
   354  
   355  func coerceToString(value interface{}) string {
   356  	return fmt.Sprintf("%v", value)
   357  }
   358  
   359  func boolVal(yamlMap generic.Map, key string, errs *[]error) bool {
   360  	switch val := yamlMap.Get(key).(type) {
   361  	case nil:
   362  		return false
   363  	case bool:
   364  		return val
   365  	case string:
   366  		return val == "true"
   367  	default:
   368  		*errs = append(*errs, fmt.Errorf(T("Expected {{.PropertyName}} to be a boolean.", map[string]interface{}{"PropertyName": key})))
   369  		return false
   370  	}
   371  }
   372  
   373  func boolOrNil(yamlMap generic.Map, key string, errs *[]error) *bool {
   374  	result := false
   375  	switch val := yamlMap.Get(key).(type) {
   376  	case nil:
   377  		return nil
   378  	case bool:
   379  		return &val
   380  	case string:
   381  		result = val == "true"
   382  		return &result
   383  	default:
   384  		*errs = append(*errs, fmt.Errorf(T("Expected {{.PropertyName}} to be a boolean.", map[string]interface{}{"PropertyName": key})))
   385  		return &result
   386  	}
   387  }
   388  func sliceOrNil(yamlMap generic.Map, key string, errs *[]error) []string {
   389  	if !yamlMap.Has(key) {
   390  		return nil
   391  	}
   392  
   393  	var err error
   394  	stringSlice := []string{}
   395  
   396  	sliceErr := fmt.Errorf(T("Expected {{.PropertyName}} to be a list of strings.", map[string]interface{}{"PropertyName": key}))
   397  
   398  	switch input := yamlMap.Get(key).(type) {
   399  	case []interface{}:
   400  		for _, value := range input {
   401  			stringValue, ok := value.(string)
   402  			if !ok {
   403  				err = sliceErr
   404  				break
   405  			}
   406  			stringSlice = append(stringSlice, stringValue)
   407  		}
   408  	default:
   409  		err = sliceErr
   410  	}
   411  
   412  	if err != nil {
   413  		*errs = append(*errs, err)
   414  		return []string{}
   415  	}
   416  
   417  	return stringSlice
   418  }
   419  
   420  func intSliceVal(yamlMap generic.Map, key string, errs *[]error) *[]int {
   421  	if !yamlMap.Has(key) {
   422  		return nil
   423  	}
   424  
   425  	err := fmt.Errorf(T("Expected {{.PropertyName}} to be a list of integers.", map[string]interface{}{"PropertyName": key}))
   426  
   427  	s, ok := yamlMap.Get(key).([]interface{})
   428  
   429  	if !ok {
   430  		*errs = append(*errs, err)
   431  		return nil
   432  	}
   433  
   434  	var intSlice []int
   435  
   436  	for _, el := range s {
   437  		intValue, ok := el.(int)
   438  
   439  		if !ok {
   440  			*errs = append(*errs, err)
   441  			return nil
   442  		}
   443  
   444  		intSlice = append(intSlice, intValue)
   445  	}
   446  
   447  	return &intSlice
   448  }
   449  
   450  func envVarOrEmptyMap(yamlMap generic.Map, errs *[]error) *map[string]interface{} {
   451  	key := "env"
   452  	switch envVars := yamlMap.Get(key).(type) {
   453  	case nil:
   454  		aMap := make(map[string]interface{}, 0)
   455  		return &aMap
   456  	case map[string]interface{}:
   457  		yamlMap.Set(key, generic.NewMap(yamlMap.Get(key)))
   458  		return envVarOrEmptyMap(yamlMap, errs)
   459  	case map[interface{}]interface{}:
   460  		yamlMap.Set(key, generic.NewMap(yamlMap.Get(key)))
   461  		return envVarOrEmptyMap(yamlMap, errs)
   462  	case generic.Map:
   463  		merrs := validateEnvVars(envVars)
   464  		if merrs != nil {
   465  			*errs = append(*errs, merrs...)
   466  			return nil
   467  		}
   468  
   469  		result := make(map[string]interface{}, envVars.Count())
   470  		generic.Each(envVars, func(key, value interface{}) {
   471  			result[key.(string)] = interfaceToString(value)
   472  		})
   473  
   474  		return &result
   475  	default:
   476  		*errs = append(*errs, fmt.Errorf(T("Expected {{.Name}} to be a set of key => value, but it was a {{.Type}}.",
   477  			map[string]interface{}{"Name": key, "Type": envVars})))
   478  		return nil
   479  	}
   480  }
   481  
   482  func validateEnvVars(input generic.Map) (errs []error) {
   483  	generic.Each(input, func(key, value interface{}) {
   484  		if value == nil {
   485  			errs = append(errs, fmt.Errorf(T("env var '{{.PropertyName}}' should not be null",
   486  				map[string]interface{}{"PropertyName": key})))
   487  		}
   488  	})
   489  	return
   490  }
   491  
   492  func interfaceToString(value interface{}) string {
   493  	if f, ok := value.(float64); ok {
   494  		return strconv.FormatFloat(f, 'f', -1, 64)
   495  	}
   496  
   497  	return fmt.Sprint(value)
   498  }
   499  
   500  func parseRoutes(input generic.Map, errs *[]error) []models.ManifestRoute {
   501  	if !input.Has("routes") {
   502  		return nil
   503  	}
   504  
   505  	genericRoutes, ok := input.Get("routes").([]interface{})
   506  	if !ok {
   507  		*errs = append(*errs, fmt.Errorf(T("'routes' should be a list")))
   508  		return nil
   509  	}
   510  
   511  	manifestRoutes := []models.ManifestRoute{}
   512  	for _, genericRoute := range genericRoutes {
   513  		route, ok := genericRoute.(map[interface{}]interface{})
   514  		if !ok {
   515  			*errs = append(*errs, fmt.Errorf(T("each route in 'routes' must have a 'route' property")))
   516  			continue
   517  		}
   518  
   519  		if routeVal, exist := route["route"]; exist {
   520  			manifestRoutes = append(manifestRoutes, models.ManifestRoute{
   521  				Route: routeVal.(string),
   522  			})
   523  		} else {
   524  			*errs = append(*errs, fmt.Errorf(T("each route in 'routes' must have a 'route' property")))
   525  		}
   526  	}
   527  
   528  	return manifestRoutes
   529  }