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