github.com/kunnos/engine@v1.13.1/cli/compose/loader/loader.go (about) 1 package loader 2 3 import ( 4 "fmt" 5 "os" 6 "path" 7 "reflect" 8 "regexp" 9 "sort" 10 "strings" 11 12 "github.com/docker/docker/cli/compose/interpolation" 13 "github.com/docker/docker/cli/compose/schema" 14 "github.com/docker/docker/cli/compose/types" 15 "github.com/docker/docker/runconfig/opts" 16 units "github.com/docker/go-units" 17 shellwords "github.com/mattn/go-shellwords" 18 "github.com/mitchellh/mapstructure" 19 yaml "gopkg.in/yaml.v2" 20 ) 21 22 var ( 23 fieldNameRegexp = regexp.MustCompile("[A-Z][a-z0-9]+") 24 ) 25 26 // ParseYAML reads the bytes from a file, parses the bytes into a mapping 27 // structure, and returns it. 28 func ParseYAML(source []byte) (types.Dict, error) { 29 var cfg interface{} 30 if err := yaml.Unmarshal(source, &cfg); err != nil { 31 return nil, err 32 } 33 cfgMap, ok := cfg.(map[interface{}]interface{}) 34 if !ok { 35 return nil, fmt.Errorf("Top-level object must be a mapping") 36 } 37 converted, err := convertToStringKeysRecursive(cfgMap, "") 38 if err != nil { 39 return nil, err 40 } 41 return converted.(types.Dict), nil 42 } 43 44 // Load reads a ConfigDetails and returns a fully loaded configuration 45 func Load(configDetails types.ConfigDetails) (*types.Config, error) { 46 if len(configDetails.ConfigFiles) < 1 { 47 return nil, fmt.Errorf("No files specified") 48 } 49 if len(configDetails.ConfigFiles) > 1 { 50 return nil, fmt.Errorf("Multiple files are not yet supported") 51 } 52 53 configDict := getConfigDict(configDetails) 54 55 if services, ok := configDict["services"]; ok { 56 if servicesDict, ok := services.(types.Dict); ok { 57 forbidden := getProperties(servicesDict, types.ForbiddenProperties) 58 59 if len(forbidden) > 0 { 60 return nil, &ForbiddenPropertiesError{Properties: forbidden} 61 } 62 } 63 } 64 65 if err := schema.Validate(configDict, schema.Version(configDict)); err != nil { 66 return nil, err 67 } 68 69 cfg := types.Config{} 70 if services, ok := configDict["services"]; ok { 71 servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", os.LookupEnv) 72 if err != nil { 73 return nil, err 74 } 75 76 servicesList, err := loadServices(servicesConfig, configDetails.WorkingDir) 77 if err != nil { 78 return nil, err 79 } 80 81 cfg.Services = servicesList 82 } 83 84 if networks, ok := configDict["networks"]; ok { 85 networksConfig, err := interpolation.Interpolate(networks.(types.Dict), "network", os.LookupEnv) 86 if err != nil { 87 return nil, err 88 } 89 90 networksMapping, err := loadNetworks(networksConfig) 91 if err != nil { 92 return nil, err 93 } 94 95 cfg.Networks = networksMapping 96 } 97 98 if volumes, ok := configDict["volumes"]; ok { 99 volumesConfig, err := interpolation.Interpolate(volumes.(types.Dict), "volume", os.LookupEnv) 100 if err != nil { 101 return nil, err 102 } 103 104 volumesMapping, err := loadVolumes(volumesConfig) 105 if err != nil { 106 return nil, err 107 } 108 109 cfg.Volumes = volumesMapping 110 } 111 112 if secrets, ok := configDict["secrets"]; ok { 113 secretsConfig, err := interpolation.Interpolate(secrets.(types.Dict), "secret", os.LookupEnv) 114 if err != nil { 115 return nil, err 116 } 117 118 secretsMapping, err := loadSecrets(secretsConfig, configDetails.WorkingDir) 119 if err != nil { 120 return nil, err 121 } 122 123 cfg.Secrets = secretsMapping 124 } 125 126 return &cfg, nil 127 } 128 129 // GetUnsupportedProperties returns the list of any unsupported properties that are 130 // used in the Compose files. 131 func GetUnsupportedProperties(configDetails types.ConfigDetails) []string { 132 unsupported := map[string]bool{} 133 134 for _, service := range getServices(getConfigDict(configDetails)) { 135 serviceDict := service.(types.Dict) 136 for _, property := range types.UnsupportedProperties { 137 if _, isSet := serviceDict[property]; isSet { 138 unsupported[property] = true 139 } 140 } 141 } 142 143 return sortedKeys(unsupported) 144 } 145 146 func sortedKeys(set map[string]bool) []string { 147 var keys []string 148 for key := range set { 149 keys = append(keys, key) 150 } 151 sort.Strings(keys) 152 return keys 153 } 154 155 // GetDeprecatedProperties returns the list of any deprecated properties that 156 // are used in the compose files. 157 func GetDeprecatedProperties(configDetails types.ConfigDetails) map[string]string { 158 return getProperties(getServices(getConfigDict(configDetails)), types.DeprecatedProperties) 159 } 160 161 func getProperties(services types.Dict, propertyMap map[string]string) map[string]string { 162 output := map[string]string{} 163 164 for _, service := range services { 165 if serviceDict, ok := service.(types.Dict); ok { 166 for property, description := range propertyMap { 167 if _, isSet := serviceDict[property]; isSet { 168 output[property] = description 169 } 170 } 171 } 172 } 173 174 return output 175 } 176 177 // ForbiddenPropertiesError is returned when there are properties in the Compose 178 // file that are forbidden. 179 type ForbiddenPropertiesError struct { 180 Properties map[string]string 181 } 182 183 func (e *ForbiddenPropertiesError) Error() string { 184 return "Configuration contains forbidden properties" 185 } 186 187 // TODO: resolve multiple files into a single config 188 func getConfigDict(configDetails types.ConfigDetails) types.Dict { 189 return configDetails.ConfigFiles[0].Config 190 } 191 192 func getServices(configDict types.Dict) types.Dict { 193 if services, ok := configDict["services"]; ok { 194 if servicesDict, ok := services.(types.Dict); ok { 195 return servicesDict 196 } 197 } 198 199 return types.Dict{} 200 } 201 202 func transform(source map[string]interface{}, target interface{}) error { 203 data := mapstructure.Metadata{} 204 config := &mapstructure.DecoderConfig{ 205 DecodeHook: mapstructure.ComposeDecodeHookFunc( 206 transformHook, 207 mapstructure.StringToTimeDurationHookFunc()), 208 Result: target, 209 Metadata: &data, 210 } 211 decoder, err := mapstructure.NewDecoder(config) 212 if err != nil { 213 return err 214 } 215 err = decoder.Decode(source) 216 // TODO: log unused keys 217 return err 218 } 219 220 func transformHook( 221 source reflect.Type, 222 target reflect.Type, 223 data interface{}, 224 ) (interface{}, error) { 225 switch target { 226 case reflect.TypeOf(types.External{}): 227 return transformExternal(data) 228 case reflect.TypeOf(make(map[string]string, 0)): 229 return transformMapStringString(source, target, data) 230 case reflect.TypeOf(types.UlimitsConfig{}): 231 return transformUlimits(data) 232 case reflect.TypeOf(types.UnitBytes(0)): 233 return loadSize(data) 234 case reflect.TypeOf(types.ServiceSecretConfig{}): 235 return transformServiceSecret(data) 236 } 237 switch target.Kind() { 238 case reflect.Struct: 239 return transformStruct(source, target, data) 240 } 241 return data, nil 242 } 243 244 // keys needs to be converted to strings for jsonschema 245 // TODO: don't use types.Dict 246 func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) { 247 if mapping, ok := value.(map[interface{}]interface{}); ok { 248 dict := make(types.Dict) 249 for key, entry := range mapping { 250 str, ok := key.(string) 251 if !ok { 252 var location string 253 if keyPrefix == "" { 254 location = "at top level" 255 } else { 256 location = fmt.Sprintf("in %s", keyPrefix) 257 } 258 return nil, fmt.Errorf("Non-string key %s: %#v", location, key) 259 } 260 var newKeyPrefix string 261 if keyPrefix == "" { 262 newKeyPrefix = str 263 } else { 264 newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str) 265 } 266 convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) 267 if err != nil { 268 return nil, err 269 } 270 dict[str] = convertedEntry 271 } 272 return dict, nil 273 } 274 if list, ok := value.([]interface{}); ok { 275 var convertedList []interface{} 276 for index, entry := range list { 277 newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index) 278 convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) 279 if err != nil { 280 return nil, err 281 } 282 convertedList = append(convertedList, convertedEntry) 283 } 284 return convertedList, nil 285 } 286 return value, nil 287 } 288 289 func loadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceConfig, error) { 290 var services []types.ServiceConfig 291 292 for name, serviceDef := range servicesDict { 293 serviceConfig, err := loadService(name, serviceDef.(types.Dict), workingDir) 294 if err != nil { 295 return nil, err 296 } 297 services = append(services, *serviceConfig) 298 } 299 300 return services, nil 301 } 302 303 func loadService(name string, serviceDict types.Dict, workingDir string) (*types.ServiceConfig, error) { 304 serviceConfig := &types.ServiceConfig{} 305 if err := transform(serviceDict, serviceConfig); err != nil { 306 return nil, err 307 } 308 serviceConfig.Name = name 309 310 if err := resolveEnvironment(serviceConfig, serviceDict, workingDir); err != nil { 311 return nil, err 312 } 313 314 if err := resolveVolumePaths(serviceConfig.Volumes, workingDir); err != nil { 315 return nil, err 316 } 317 318 return serviceConfig, nil 319 } 320 321 func resolveEnvironment(serviceConfig *types.ServiceConfig, serviceDict types.Dict, workingDir string) error { 322 environment := make(map[string]string) 323 324 if envFileVal, ok := serviceDict["env_file"]; ok { 325 envFiles := loadStringOrListOfStrings(envFileVal) 326 327 var envVars []string 328 329 for _, file := range envFiles { 330 filePath := absPath(workingDir, file) 331 fileVars, err := opts.ParseEnvFile(filePath) 332 if err != nil { 333 return err 334 } 335 envVars = append(envVars, fileVars...) 336 } 337 338 for k, v := range opts.ConvertKVStringsToMap(envVars) { 339 environment[k] = v 340 } 341 } 342 343 for k, v := range serviceConfig.Environment { 344 environment[k] = v 345 } 346 347 serviceConfig.Environment = environment 348 349 return nil 350 } 351 352 func resolveVolumePaths(volumes []string, workingDir string) error { 353 for i, mapping := range volumes { 354 parts := strings.SplitN(mapping, ":", 2) 355 if len(parts) == 1 { 356 continue 357 } 358 359 if strings.HasPrefix(parts[0], ".") { 360 parts[0] = absPath(workingDir, parts[0]) 361 } 362 parts[0] = expandUser(parts[0]) 363 364 volumes[i] = strings.Join(parts, ":") 365 } 366 367 return nil 368 } 369 370 // TODO: make this more robust 371 func expandUser(path string) string { 372 if strings.HasPrefix(path, "~") { 373 return strings.Replace(path, "~", os.Getenv("HOME"), 1) 374 } 375 return path 376 } 377 378 func transformUlimits(data interface{}) (interface{}, error) { 379 switch value := data.(type) { 380 case int: 381 return types.UlimitsConfig{Single: value}, nil 382 case types.Dict: 383 ulimit := types.UlimitsConfig{} 384 ulimit.Soft = value["soft"].(int) 385 ulimit.Hard = value["hard"].(int) 386 return ulimit, nil 387 default: 388 return data, fmt.Errorf("invalid type %T for ulimits", value) 389 } 390 } 391 392 func loadNetworks(source types.Dict) (map[string]types.NetworkConfig, error) { 393 networks := make(map[string]types.NetworkConfig) 394 err := transform(source, &networks) 395 if err != nil { 396 return networks, err 397 } 398 for name, network := range networks { 399 if network.External.External && network.External.Name == "" { 400 network.External.Name = name 401 networks[name] = network 402 } 403 } 404 return networks, nil 405 } 406 407 func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) { 408 volumes := make(map[string]types.VolumeConfig) 409 err := transform(source, &volumes) 410 if err != nil { 411 return volumes, err 412 } 413 for name, volume := range volumes { 414 if volume.External.External && volume.External.Name == "" { 415 volume.External.Name = name 416 volumes[name] = volume 417 } 418 } 419 return volumes, nil 420 } 421 422 // TODO: remove duplicate with networks/volumes 423 func loadSecrets(source types.Dict, workingDir string) (map[string]types.SecretConfig, error) { 424 secrets := make(map[string]types.SecretConfig) 425 if err := transform(source, &secrets); err != nil { 426 return secrets, err 427 } 428 for name, secret := range secrets { 429 if secret.External.External && secret.External.Name == "" { 430 secret.External.Name = name 431 secrets[name] = secret 432 } 433 if secret.File != "" { 434 secret.File = absPath(workingDir, secret.File) 435 } 436 } 437 return secrets, nil 438 } 439 440 func absPath(workingDir string, filepath string) string { 441 if path.IsAbs(filepath) { 442 return filepath 443 } 444 return path.Join(workingDir, filepath) 445 } 446 447 func transformStruct( 448 source reflect.Type, 449 target reflect.Type, 450 data interface{}, 451 ) (interface{}, error) { 452 structValue, ok := data.(map[string]interface{}) 453 if !ok { 454 // FIXME: this is necessary because of convertToStringKeysRecursive 455 structValue, ok = data.(types.Dict) 456 if !ok { 457 panic(fmt.Sprintf( 458 "transformStruct called with non-map type: %T, %s", data, data)) 459 } 460 } 461 462 var err error 463 for i := 0; i < target.NumField(); i++ { 464 field := target.Field(i) 465 fieldTag := field.Tag.Get("compose") 466 467 yamlName := toYAMLName(field.Name) 468 value, ok := structValue[yamlName] 469 if !ok { 470 continue 471 } 472 473 structValue[yamlName], err = convertField( 474 fieldTag, reflect.TypeOf(value), field.Type, value) 475 if err != nil { 476 return nil, fmt.Errorf("field %s: %s", yamlName, err.Error()) 477 } 478 } 479 return structValue, nil 480 } 481 482 func transformMapStringString( 483 source reflect.Type, 484 target reflect.Type, 485 data interface{}, 486 ) (interface{}, error) { 487 switch value := data.(type) { 488 case map[string]interface{}: 489 return toMapStringString(value), nil 490 case types.Dict: 491 return toMapStringString(value), nil 492 case map[string]string: 493 return value, nil 494 default: 495 return data, fmt.Errorf("invalid type %T for map[string]string", value) 496 } 497 } 498 499 func convertField( 500 fieldTag string, 501 source reflect.Type, 502 target reflect.Type, 503 data interface{}, 504 ) (interface{}, error) { 505 switch fieldTag { 506 case "": 507 return data, nil 508 case "healthcheck": 509 return loadHealthcheck(data) 510 case "list_or_dict_equals": 511 return loadMappingOrList(data, "="), nil 512 case "list_or_dict_colon": 513 return loadMappingOrList(data, ":"), nil 514 case "list_or_struct_map": 515 return loadListOrStructMap(data, target) 516 case "string_or_list": 517 return loadStringOrListOfStrings(data), nil 518 case "list_of_strings_or_numbers": 519 return loadListOfStringsOrNumbers(data), nil 520 case "shell_command": 521 return loadShellCommand(data) 522 case "size": 523 return loadSize(data) 524 case "-": 525 return nil, nil 526 } 527 return data, nil 528 } 529 530 func transformExternal(data interface{}) (interface{}, error) { 531 switch value := data.(type) { 532 case bool: 533 return map[string]interface{}{"external": value}, nil 534 case types.Dict: 535 return map[string]interface{}{"external": true, "name": value["name"]}, nil 536 case map[string]interface{}: 537 return map[string]interface{}{"external": true, "name": value["name"]}, nil 538 default: 539 return data, fmt.Errorf("invalid type %T for external", value) 540 } 541 } 542 543 func transformServiceSecret(data interface{}) (interface{}, error) { 544 switch value := data.(type) { 545 case string: 546 return map[string]interface{}{"source": value}, nil 547 case types.Dict: 548 return data, nil 549 case map[string]interface{}: 550 return data, nil 551 default: 552 return data, fmt.Errorf("invalid type %T for external", value) 553 } 554 555 } 556 557 func toYAMLName(name string) string { 558 nameParts := fieldNameRegexp.FindAllString(name, -1) 559 for i, p := range nameParts { 560 nameParts[i] = strings.ToLower(p) 561 } 562 return strings.Join(nameParts, "_") 563 } 564 565 func loadListOrStructMap(value interface{}, target reflect.Type) (interface{}, error) { 566 if list, ok := value.([]interface{}); ok { 567 mapValue := map[interface{}]interface{}{} 568 for _, name := range list { 569 mapValue[name] = nil 570 } 571 return mapValue, nil 572 } 573 574 return value, nil 575 } 576 577 func loadListOfStringsOrNumbers(value interface{}) []string { 578 list := value.([]interface{}) 579 result := make([]string, len(list)) 580 for i, item := range list { 581 result[i] = fmt.Sprint(item) 582 } 583 return result 584 } 585 586 func loadStringOrListOfStrings(value interface{}) []string { 587 if list, ok := value.([]interface{}); ok { 588 result := make([]string, len(list)) 589 for i, item := range list { 590 result[i] = fmt.Sprint(item) 591 } 592 return result 593 } 594 return []string{value.(string)} 595 } 596 597 func loadMappingOrList(mappingOrList interface{}, sep string) map[string]string { 598 if mapping, ok := mappingOrList.(types.Dict); ok { 599 return toMapStringString(mapping) 600 } 601 if list, ok := mappingOrList.([]interface{}); ok { 602 result := make(map[string]string) 603 for _, value := range list { 604 parts := strings.SplitN(value.(string), sep, 2) 605 if len(parts) == 1 { 606 result[parts[0]] = "" 607 } else { 608 result[parts[0]] = parts[1] 609 } 610 } 611 return result 612 } 613 panic(fmt.Errorf("expected a map or a slice, got: %#v", mappingOrList)) 614 } 615 616 func loadShellCommand(value interface{}) (interface{}, error) { 617 if str, ok := value.(string); ok { 618 return shellwords.Parse(str) 619 } 620 return value, nil 621 } 622 623 func loadHealthcheck(value interface{}) (interface{}, error) { 624 if str, ok := value.(string); ok { 625 return append([]string{"CMD-SHELL"}, str), nil 626 } 627 return value, nil 628 } 629 630 func loadSize(value interface{}) (int64, error) { 631 switch value := value.(type) { 632 case int: 633 return int64(value), nil 634 case string: 635 return units.RAMInBytes(value) 636 } 637 panic(fmt.Errorf("invalid type for size %T", value)) 638 } 639 640 func toMapStringString(value map[string]interface{}) map[string]string { 641 output := make(map[string]string) 642 for key, value := range value { 643 output[key] = toString(value) 644 } 645 return output 646 } 647 648 func toString(value interface{}) string { 649 if value == nil { 650 return "" 651 } 652 return fmt.Sprint(value) 653 }