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 }