github.com/loggregator/cli@v6.33.1-0.20180224010324-82334f081791+incompatible/cf/manifest/manifest.go (about)

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