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 }