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