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