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