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 }