github.com/AliyunContainerService/cli@v0.0.0-20181009023821-814ced4b30d0/cli/compose/loader/loader.go (about) 1 package loader 2 3 import ( 4 "fmt" 5 "path" 6 "path/filepath" 7 "reflect" 8 "sort" 9 "strings" 10 "time" 11 12 interp "github.com/docker/cli/cli/compose/interpolation" 13 "github.com/docker/cli/cli/compose/schema" 14 "github.com/docker/cli/cli/compose/template" 15 "github.com/docker/cli/cli/compose/types" 16 "github.com/docker/cli/opts" 17 "github.com/docker/docker/api/types/versions" 18 "github.com/docker/go-connections/nat" 19 units "github.com/docker/go-units" 20 shellwords "github.com/mattn/go-shellwords" 21 "github.com/mitchellh/mapstructure" 22 "github.com/pkg/errors" 23 "github.com/sirupsen/logrus" 24 yaml "gopkg.in/yaml.v2" 25 ) 26 27 // Options supported by Load 28 type Options struct { 29 // Skip schema validation 30 SkipValidation bool 31 // Skip interpolation 32 SkipInterpolation bool 33 // Interpolation options 34 Interpolate *interp.Options 35 } 36 37 // ParseYAML reads the bytes from a file, parses the bytes into a mapping 38 // structure, and returns it. 39 func ParseYAML(source []byte) (map[string]interface{}, error) { 40 var cfg interface{} 41 if err := yaml.Unmarshal(source, &cfg); err != nil { 42 return nil, err 43 } 44 cfgMap, ok := cfg.(map[interface{}]interface{}) 45 if !ok { 46 return nil, errors.Errorf("Top-level object must be a mapping") 47 } 48 converted, err := convertToStringKeysRecursive(cfgMap, "") 49 if err != nil { 50 return nil, err 51 } 52 return converted.(map[string]interface{}), nil 53 } 54 55 // Load reads a ConfigDetails and returns a fully loaded configuration 56 func Load(configDetails types.ConfigDetails, options ...func(*Options)) (*types.Config, error) { 57 if len(configDetails.ConfigFiles) < 1 { 58 return nil, errors.Errorf("No files specified") 59 } 60 61 opts := &Options{ 62 Interpolate: &interp.Options{ 63 Substitute: template.Substitute, 64 LookupValue: configDetails.LookupEnv, 65 TypeCastMapping: interpolateTypeCastMapping, 66 }, 67 } 68 69 for _, op := range options { 70 op(opts) 71 } 72 73 configs := []*types.Config{} 74 var err error 75 76 for _, file := range configDetails.ConfigFiles { 77 configDict := file.Config 78 version := schema.Version(configDict) 79 if configDetails.Version == "" { 80 configDetails.Version = version 81 } 82 if configDetails.Version != version { 83 return nil, errors.Errorf("version mismatched between two composefiles : %v and %v", configDetails.Version, version) 84 } 85 86 if err := validateForbidden(configDict); err != nil { 87 return nil, err 88 } 89 90 if !opts.SkipInterpolation { 91 configDict, err = interpolateConfig(configDict, *opts.Interpolate) 92 if err != nil { 93 return nil, err 94 } 95 } 96 97 if !opts.SkipValidation { 98 if err := schema.Validate(configDict, configDetails.Version); err != nil { 99 return nil, err 100 } 101 } 102 103 cfg, err := loadSections(configDict, configDetails) 104 if err != nil { 105 return nil, err 106 } 107 cfg.Filename = file.Filename 108 109 configs = append(configs, cfg) 110 } 111 112 return merge(configs) 113 } 114 115 func validateForbidden(configDict map[string]interface{}) error { 116 servicesDict, ok := configDict["services"].(map[string]interface{}) 117 if !ok { 118 return nil 119 } 120 forbidden := getProperties(servicesDict, types.ForbiddenProperties) 121 if len(forbidden) > 0 { 122 return &ForbiddenPropertiesError{Properties: forbidden} 123 } 124 return nil 125 } 126 127 func loadSections(config map[string]interface{}, configDetails types.ConfigDetails) (*types.Config, error) { 128 var err error 129 cfg := types.Config{ 130 Version: schema.Version(config), 131 } 132 133 var loaders = []struct { 134 key string 135 fnc func(config map[string]interface{}) error 136 }{ 137 { 138 key: "services", 139 fnc: func(config map[string]interface{}) error { 140 cfg.Services, err = LoadServices(config, configDetails.WorkingDir, configDetails.LookupEnv) 141 return err 142 }, 143 }, 144 { 145 key: "networks", 146 fnc: func(config map[string]interface{}) error { 147 cfg.Networks, err = LoadNetworks(config, configDetails.Version) 148 return err 149 }, 150 }, 151 { 152 key: "volumes", 153 fnc: func(config map[string]interface{}) error { 154 cfg.Volumes, err = LoadVolumes(config, configDetails.Version) 155 return err 156 }, 157 }, 158 { 159 key: "secrets", 160 fnc: func(config map[string]interface{}) error { 161 cfg.Secrets, err = LoadSecrets(config, configDetails) 162 return err 163 }, 164 }, 165 { 166 key: "configs", 167 fnc: func(config map[string]interface{}) error { 168 cfg.Configs, err = LoadConfigObjs(config, configDetails) 169 return err 170 }, 171 }, 172 } 173 for _, loader := range loaders { 174 if err := loader.fnc(getSection(config, loader.key)); err != nil { 175 return nil, err 176 } 177 } 178 cfg.Extras = getExtras(config) 179 return &cfg, nil 180 } 181 182 func getSection(config map[string]interface{}, key string) map[string]interface{} { 183 section, ok := config[key] 184 if !ok { 185 return make(map[string]interface{}) 186 } 187 return section.(map[string]interface{}) 188 } 189 190 // GetUnsupportedProperties returns the list of any unsupported properties that are 191 // used in the Compose files. 192 func GetUnsupportedProperties(configDicts ...map[string]interface{}) []string { 193 unsupported := map[string]bool{} 194 195 for _, configDict := range configDicts { 196 for _, service := range getServices(configDict) { 197 serviceDict := service.(map[string]interface{}) 198 for _, property := range types.UnsupportedProperties { 199 if _, isSet := serviceDict[property]; isSet { 200 unsupported[property] = true 201 } 202 } 203 } 204 } 205 206 return sortedKeys(unsupported) 207 } 208 209 func sortedKeys(set map[string]bool) []string { 210 var keys []string 211 for key := range set { 212 keys = append(keys, key) 213 } 214 sort.Strings(keys) 215 return keys 216 } 217 218 // GetDeprecatedProperties returns the list of any deprecated properties that 219 // are used in the compose files. 220 func GetDeprecatedProperties(configDicts ...map[string]interface{}) map[string]string { 221 deprecated := map[string]string{} 222 223 for _, configDict := range configDicts { 224 deprecatedProperties := getProperties(getServices(configDict), types.DeprecatedProperties) 225 for key, value := range deprecatedProperties { 226 deprecated[key] = value 227 } 228 } 229 230 return deprecated 231 } 232 233 func getProperties(services map[string]interface{}, propertyMap map[string]string) map[string]string { 234 output := map[string]string{} 235 236 for _, service := range services { 237 if serviceDict, ok := service.(map[string]interface{}); ok { 238 for property, description := range propertyMap { 239 if _, isSet := serviceDict[property]; isSet { 240 output[property] = description 241 } 242 } 243 } 244 } 245 246 return output 247 } 248 249 // ForbiddenPropertiesError is returned when there are properties in the Compose 250 // file that are forbidden. 251 type ForbiddenPropertiesError struct { 252 Properties map[string]string 253 } 254 255 func (e *ForbiddenPropertiesError) Error() string { 256 return "Configuration contains forbidden properties" 257 } 258 259 func getServices(configDict map[string]interface{}) map[string]interface{} { 260 if services, ok := configDict["services"]; ok { 261 if servicesDict, ok := services.(map[string]interface{}); ok { 262 return servicesDict 263 } 264 } 265 266 return map[string]interface{}{} 267 } 268 269 // Transform converts the source into the target struct with compose types transformer 270 // and the specified transformers if any. 271 func Transform(source interface{}, target interface{}, additionalTransformers ...Transformer) error { 272 data := mapstructure.Metadata{} 273 config := &mapstructure.DecoderConfig{ 274 DecodeHook: mapstructure.ComposeDecodeHookFunc( 275 createTransformHook(additionalTransformers...), 276 mapstructure.StringToTimeDurationHookFunc()), 277 Result: target, 278 Metadata: &data, 279 } 280 decoder, err := mapstructure.NewDecoder(config) 281 if err != nil { 282 return err 283 } 284 return decoder.Decode(source) 285 } 286 287 // Transformer defines a map to type transformer 288 type Transformer struct { 289 TypeOf reflect.Type 290 Func func(interface{}) (interface{}, error) 291 } 292 293 func createTransformHook(additionalTransformers ...Transformer) mapstructure.DecodeHookFuncType { 294 transforms := map[reflect.Type]func(interface{}) (interface{}, error){ 295 reflect.TypeOf(types.External{}): transformExternal, 296 reflect.TypeOf(types.HealthCheckTest{}): transformHealthCheckTest, 297 reflect.TypeOf(types.ShellCommand{}): transformShellCommand, 298 reflect.TypeOf(types.StringList{}): transformStringList, 299 reflect.TypeOf(map[string]string{}): transformMapStringString, 300 reflect.TypeOf(types.UlimitsConfig{}): transformUlimits, 301 reflect.TypeOf(types.UnitBytes(0)): transformSize, 302 reflect.TypeOf([]types.ServicePortConfig{}): transformServicePort, 303 reflect.TypeOf(types.ServiceSecretConfig{}): transformStringSourceMap, 304 reflect.TypeOf(types.ServiceConfigObjConfig{}): transformStringSourceMap, 305 reflect.TypeOf(types.StringOrNumberList{}): transformStringOrNumberList, 306 reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): transformServiceNetworkMap, 307 reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true), 308 reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false), 309 reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false), 310 reflect.TypeOf(types.HostsList{}): transformListOrMappingFunc(":", false), 311 reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig, 312 reflect.TypeOf(types.BuildConfig{}): transformBuildConfig, 313 reflect.TypeOf(types.Duration(0)): transformStringToDuration, 314 } 315 316 for _, transformer := range additionalTransformers { 317 transforms[transformer.TypeOf] = transformer.Func 318 } 319 320 return func(_ reflect.Type, target reflect.Type, data interface{}) (interface{}, error) { 321 transform, ok := transforms[target] 322 if !ok { 323 return data, nil 324 } 325 return transform(data) 326 } 327 } 328 329 // keys needs to be converted to strings for jsonschema 330 func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) { 331 if mapping, ok := value.(map[interface{}]interface{}); ok { 332 dict := make(map[string]interface{}) 333 for key, entry := range mapping { 334 str, ok := key.(string) 335 if !ok { 336 return nil, formatInvalidKeyError(keyPrefix, key) 337 } 338 var newKeyPrefix string 339 if keyPrefix == "" { 340 newKeyPrefix = str 341 } else { 342 newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str) 343 } 344 convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) 345 if err != nil { 346 return nil, err 347 } 348 dict[str] = convertedEntry 349 } 350 return dict, nil 351 } 352 if list, ok := value.([]interface{}); ok { 353 var convertedList []interface{} 354 for index, entry := range list { 355 newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index) 356 convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) 357 if err != nil { 358 return nil, err 359 } 360 convertedList = append(convertedList, convertedEntry) 361 } 362 return convertedList, nil 363 } 364 return value, nil 365 } 366 367 func formatInvalidKeyError(keyPrefix string, key interface{}) error { 368 var location string 369 if keyPrefix == "" { 370 location = "at top level" 371 } else { 372 location = fmt.Sprintf("in %s", keyPrefix) 373 } 374 return errors.Errorf("Non-string key %s: %#v", location, key) 375 } 376 377 // LoadServices produces a ServiceConfig map from a compose file Dict 378 // the servicesDict is not validated if directly used. Use Load() to enable validation 379 func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) { 380 var services []types.ServiceConfig 381 382 for name, serviceDef := range servicesDict { 383 serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, lookupEnv) 384 if err != nil { 385 return nil, err 386 } 387 services = append(services, *serviceConfig) 388 } 389 390 return services, nil 391 } 392 393 // LoadService produces a single ServiceConfig from a compose file Dict 394 // the serviceDict is not validated if directly used. Use Load() to enable validation 395 func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) { 396 serviceConfig := &types.ServiceConfig{} 397 if err := Transform(serviceDict, serviceConfig); err != nil { 398 return nil, err 399 } 400 serviceConfig.Name = name 401 402 if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil { 403 return nil, err 404 } 405 406 if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil { 407 return nil, err 408 } 409 410 serviceConfig.Extras = getExtras(serviceDict) 411 412 return serviceConfig, nil 413 } 414 415 func loadExtras(name string, source map[string]interface{}) map[string]interface{} { 416 if dict, ok := source[name].(map[string]interface{}); ok { 417 return getExtras(dict) 418 } 419 return nil 420 } 421 422 func getExtras(dict map[string]interface{}) map[string]interface{} { 423 extras := map[string]interface{}{} 424 for key, value := range dict { 425 if strings.HasPrefix(key, "x-") { 426 extras[key] = value 427 } 428 } 429 if len(extras) == 0 { 430 return nil 431 } 432 return extras 433 } 434 435 func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv template.Mapping) { 436 for k, v := range vars { 437 interpolatedV, ok := lookupEnv(k) 438 if (v == nil || *v == "") && ok { 439 // lookupEnv is prioritized over vars 440 environment[k] = &interpolatedV 441 } else { 442 environment[k] = v 443 } 444 } 445 } 446 447 func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error { 448 environment := make(map[string]*string) 449 450 if len(serviceConfig.EnvFile) > 0 { 451 var envVars []string 452 453 for _, file := range serviceConfig.EnvFile { 454 filePath := absPath(workingDir, file) 455 fileVars, err := opts.ParseEnvFile(filePath) 456 if err != nil { 457 return err 458 } 459 envVars = append(envVars, fileVars...) 460 } 461 updateEnvironment(environment, 462 opts.ConvertKVStringsToMapWithNil(envVars), lookupEnv) 463 } 464 465 updateEnvironment(environment, serviceConfig.Environment, lookupEnv) 466 serviceConfig.Environment = environment 467 return nil 468 } 469 470 func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error { 471 for i, volume := range volumes { 472 if volume.Type != "bind" { 473 continue 474 } 475 476 if volume.Source == "" { 477 return errors.New(`invalid mount config for type "bind": field Source must not be empty`) 478 } 479 480 filePath := expandUser(volume.Source, lookupEnv) 481 // Check for a Unix absolute path first, to handle a Windows client 482 // with a Unix daemon. This handles a Windows client connecting to a 483 // Unix daemon. Note that this is not required for Docker for Windows 484 // when specifying a local Windows path, because Docker for Windows 485 // translates the Windows path into a valid path within the VM. 486 if !path.IsAbs(filePath) { 487 filePath = absPath(workingDir, filePath) 488 } 489 volume.Source = filePath 490 volumes[i] = volume 491 } 492 return nil 493 } 494 495 // TODO: make this more robust 496 func expandUser(path string, lookupEnv template.Mapping) string { 497 if strings.HasPrefix(path, "~") { 498 home, ok := lookupEnv("HOME") 499 if !ok { 500 logrus.Warn("cannot expand '~', because the environment lacks HOME") 501 return path 502 } 503 return strings.Replace(path, "~", home, 1) 504 } 505 return path 506 } 507 508 func transformUlimits(data interface{}) (interface{}, error) { 509 switch value := data.(type) { 510 case int: 511 return types.UlimitsConfig{Single: value}, nil 512 case map[string]interface{}: 513 ulimit := types.UlimitsConfig{} 514 ulimit.Soft = value["soft"].(int) 515 ulimit.Hard = value["hard"].(int) 516 return ulimit, nil 517 default: 518 return data, errors.Errorf("invalid type %T for ulimits", value) 519 } 520 } 521 522 // LoadNetworks produces a NetworkConfig map from a compose file Dict 523 // the source Dict is not validated if directly used. Use Load() to enable validation 524 func LoadNetworks(source map[string]interface{}, version string) (map[string]types.NetworkConfig, error) { 525 networks := make(map[string]types.NetworkConfig) 526 err := Transform(source, &networks) 527 if err != nil { 528 return networks, err 529 } 530 for name, network := range networks { 531 if !network.External.External { 532 continue 533 } 534 switch { 535 case network.External.Name != "": 536 if network.Name != "" { 537 return nil, errors.Errorf("network %s: network.external.name and network.name conflict; only use network.name", name) 538 } 539 if versions.GreaterThanOrEqualTo(version, "3.5") { 540 logrus.Warnf("network %s: network.external.name is deprecated in favor of network.name", name) 541 } 542 network.Name = network.External.Name 543 network.External.Name = "" 544 case network.Name == "": 545 network.Name = name 546 } 547 network.Extras = loadExtras(name, source) 548 networks[name] = network 549 } 550 return networks, nil 551 } 552 553 func externalVolumeError(volume, key string) error { 554 return errors.Errorf( 555 "conflicting parameters \"external\" and %q specified for volume %q", 556 key, volume) 557 } 558 559 // LoadVolumes produces a VolumeConfig map from a compose file Dict 560 // the source Dict is not validated if directly used. Use Load() to enable validation 561 func LoadVolumes(source map[string]interface{}, version string) (map[string]types.VolumeConfig, error) { 562 volumes := make(map[string]types.VolumeConfig) 563 if err := Transform(source, &volumes); err != nil { 564 return volumes, err 565 } 566 567 for name, volume := range volumes { 568 if !volume.External.External { 569 continue 570 } 571 switch { 572 case volume.Driver != "": 573 return nil, externalVolumeError(name, "driver") 574 case len(volume.DriverOpts) > 0: 575 return nil, externalVolumeError(name, "driver_opts") 576 case len(volume.Labels) > 0: 577 return nil, externalVolumeError(name, "labels") 578 case volume.External.Name != "": 579 if volume.Name != "" { 580 return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name) 581 } 582 if versions.GreaterThanOrEqualTo(version, "3.4") { 583 logrus.Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name) 584 } 585 volume.Name = volume.External.Name 586 volume.External.Name = "" 587 case volume.Name == "": 588 volume.Name = name 589 } 590 volume.Extras = loadExtras(name, source) 591 volumes[name] = volume 592 } 593 return volumes, nil 594 } 595 596 // LoadSecrets produces a SecretConfig map from a compose file Dict 597 // the source Dict is not validated if directly used. Use Load() to enable validation 598 func LoadSecrets(source map[string]interface{}, details types.ConfigDetails) (map[string]types.SecretConfig, error) { 599 secrets := make(map[string]types.SecretConfig) 600 if err := Transform(source, &secrets); err != nil { 601 return secrets, err 602 } 603 for name, secret := range secrets { 604 obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details) 605 if err != nil { 606 return nil, err 607 } 608 secretConfig := types.SecretConfig(obj) 609 secretConfig.Extras = loadExtras(name, source) 610 secrets[name] = secretConfig 611 } 612 return secrets, nil 613 } 614 615 // LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict 616 // the source Dict is not validated if directly used. Use Load() to enable validation 617 func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails) (map[string]types.ConfigObjConfig, error) { 618 configs := make(map[string]types.ConfigObjConfig) 619 if err := Transform(source, &configs); err != nil { 620 return configs, err 621 } 622 for name, config := range configs { 623 obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details) 624 if err != nil { 625 return nil, err 626 } 627 configConfig := types.ConfigObjConfig(obj) 628 configConfig.Extras = loadExtras(name, source) 629 configs[name] = configConfig 630 } 631 return configs, nil 632 } 633 634 func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails) (types.FileObjectConfig, error) { 635 // if "external: true" 636 if obj.External.External { 637 // handle deprecated external.name 638 if obj.External.Name != "" { 639 if obj.Name != "" { 640 return obj, errors.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name) 641 } 642 if versions.GreaterThanOrEqualTo(details.Version, "3.5") { 643 logrus.Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name) 644 } 645 obj.Name = obj.External.Name 646 obj.External.Name = "" 647 } else { 648 if obj.Name == "" { 649 obj.Name = name 650 } 651 } 652 // if not "external: true" 653 } else { 654 obj.File = absPath(details.WorkingDir, obj.File) 655 } 656 657 return obj, nil 658 } 659 660 func absPath(workingDir string, filePath string) string { 661 if filepath.IsAbs(filePath) { 662 return filePath 663 } 664 return filepath.Join(workingDir, filePath) 665 } 666 667 func transformMapStringString(data interface{}) (interface{}, error) { 668 switch value := data.(type) { 669 case map[string]interface{}: 670 return toMapStringString(value, false), nil 671 case map[string]string: 672 return value, nil 673 default: 674 return data, errors.Errorf("invalid type %T for map[string]string", value) 675 } 676 } 677 678 func transformExternal(data interface{}) (interface{}, error) { 679 switch value := data.(type) { 680 case bool: 681 return map[string]interface{}{"external": value}, nil 682 case map[string]interface{}: 683 return map[string]interface{}{"external": true, "name": value["name"]}, nil 684 default: 685 return data, errors.Errorf("invalid type %T for external", value) 686 } 687 } 688 689 func transformServicePort(data interface{}) (interface{}, error) { 690 switch entries := data.(type) { 691 case []interface{}: 692 // We process the list instead of individual items here. 693 // The reason is that one entry might be mapped to multiple ServicePortConfig. 694 // Therefore we take an input of a list and return an output of a list. 695 ports := []interface{}{} 696 for _, entry := range entries { 697 switch value := entry.(type) { 698 case int: 699 v, err := toServicePortConfigs(fmt.Sprint(value)) 700 if err != nil { 701 return data, err 702 } 703 ports = append(ports, v...) 704 case string: 705 v, err := toServicePortConfigs(value) 706 if err != nil { 707 return data, err 708 } 709 ports = append(ports, v...) 710 case map[string]interface{}: 711 ports = append(ports, value) 712 default: 713 return data, errors.Errorf("invalid type %T for port", value) 714 } 715 } 716 return ports, nil 717 default: 718 return data, errors.Errorf("invalid type %T for port", entries) 719 } 720 } 721 722 func transformStringSourceMap(data interface{}) (interface{}, error) { 723 switch value := data.(type) { 724 case string: 725 return map[string]interface{}{"source": value}, nil 726 case map[string]interface{}: 727 return data, nil 728 default: 729 return data, errors.Errorf("invalid type %T for secret", value) 730 } 731 } 732 733 func transformBuildConfig(data interface{}) (interface{}, error) { 734 switch value := data.(type) { 735 case string: 736 return map[string]interface{}{"context": value}, nil 737 case map[string]interface{}: 738 return data, nil 739 default: 740 return data, errors.Errorf("invalid type %T for service build", value) 741 } 742 } 743 744 func transformServiceVolumeConfig(data interface{}) (interface{}, error) { 745 switch value := data.(type) { 746 case string: 747 return ParseVolume(value) 748 case map[string]interface{}: 749 return data, nil 750 default: 751 return data, errors.Errorf("invalid type %T for service volume", value) 752 } 753 } 754 755 func transformServiceNetworkMap(value interface{}) (interface{}, error) { 756 if list, ok := value.([]interface{}); ok { 757 mapValue := map[interface{}]interface{}{} 758 for _, name := range list { 759 mapValue[name] = nil 760 } 761 return mapValue, nil 762 } 763 return value, nil 764 } 765 766 func transformStringOrNumberList(value interface{}) (interface{}, error) { 767 list := value.([]interface{}) 768 result := make([]string, len(list)) 769 for i, item := range list { 770 result[i] = fmt.Sprint(item) 771 } 772 return result, nil 773 } 774 775 func transformStringList(data interface{}) (interface{}, error) { 776 switch value := data.(type) { 777 case string: 778 return []string{value}, nil 779 case []interface{}: 780 return value, nil 781 default: 782 return data, errors.Errorf("invalid type %T for string list", value) 783 } 784 } 785 786 func transformMappingOrListFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) { 787 return func(data interface{}) (interface{}, error) { 788 return transformMappingOrList(data, sep, allowNil), nil 789 } 790 } 791 792 func transformListOrMappingFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) { 793 return func(data interface{}) (interface{}, error) { 794 return transformListOrMapping(data, sep, allowNil), nil 795 } 796 } 797 798 func transformListOrMapping(listOrMapping interface{}, sep string, allowNil bool) interface{} { 799 switch value := listOrMapping.(type) { 800 case map[string]interface{}: 801 return toStringList(value, sep, allowNil) 802 case []interface{}: 803 return listOrMapping 804 } 805 panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping)) 806 } 807 808 func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} { 809 switch value := mappingOrList.(type) { 810 case map[string]interface{}: 811 return toMapStringString(value, allowNil) 812 case ([]interface{}): 813 result := make(map[string]interface{}) 814 for _, value := range value { 815 parts := strings.SplitN(value.(string), sep, 2) 816 key := parts[0] 817 switch { 818 case len(parts) == 1 && allowNil: 819 result[key] = nil 820 case len(parts) == 1 && !allowNil: 821 result[key] = "" 822 default: 823 result[key] = parts[1] 824 } 825 } 826 return result 827 } 828 panic(errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList)) 829 } 830 831 func transformShellCommand(value interface{}) (interface{}, error) { 832 if str, ok := value.(string); ok { 833 return shellwords.Parse(str) 834 } 835 return value, nil 836 } 837 838 func transformHealthCheckTest(data interface{}) (interface{}, error) { 839 switch value := data.(type) { 840 case string: 841 return append([]string{"CMD-SHELL"}, value), nil 842 case []interface{}: 843 return value, nil 844 default: 845 return value, errors.Errorf("invalid type %T for healthcheck.test", value) 846 } 847 } 848 849 func transformSize(value interface{}) (interface{}, error) { 850 switch value := value.(type) { 851 case int: 852 return int64(value), nil 853 case string: 854 return units.RAMInBytes(value) 855 } 856 panic(errors.Errorf("invalid type for size %T", value)) 857 } 858 859 func transformStringToDuration(value interface{}) (interface{}, error) { 860 switch value := value.(type) { 861 case string: 862 d, err := time.ParseDuration(value) 863 if err != nil { 864 return value, err 865 } 866 return types.Duration(d), nil 867 default: 868 return value, errors.Errorf("invalid type %T for duration", value) 869 } 870 } 871 872 func toServicePortConfigs(value string) ([]interface{}, error) { 873 var portConfigs []interface{} 874 875 ports, portBindings, err := nat.ParsePortSpecs([]string{value}) 876 if err != nil { 877 return nil, err 878 } 879 // We need to sort the key of the ports to make sure it is consistent 880 keys := []string{} 881 for port := range ports { 882 keys = append(keys, string(port)) 883 } 884 sort.Strings(keys) 885 886 for _, key := range keys { 887 // Reuse ConvertPortToPortConfig so that it is consistent 888 portConfig, err := opts.ConvertPortToPortConfig(nat.Port(key), portBindings) 889 if err != nil { 890 return nil, err 891 } 892 for _, p := range portConfig { 893 portConfigs = append(portConfigs, types.ServicePortConfig{ 894 Protocol: string(p.Protocol), 895 Target: p.TargetPort, 896 Published: p.PublishedPort, 897 Mode: string(p.PublishMode), 898 }) 899 } 900 } 901 902 return portConfigs, nil 903 } 904 905 func toMapStringString(value map[string]interface{}, allowNil bool) map[string]interface{} { 906 output := make(map[string]interface{}) 907 for key, value := range value { 908 output[key] = toString(value, allowNil) 909 } 910 return output 911 } 912 913 func toString(value interface{}, allowNil bool) interface{} { 914 switch { 915 case value != nil: 916 return fmt.Sprint(value) 917 case allowNil: 918 return nil 919 default: 920 return "" 921 } 922 } 923 924 func toStringList(value map[string]interface{}, separator string, allowNil bool) []string { 925 output := []string{} 926 for key, value := range value { 927 if value == nil && !allowNil { 928 continue 929 } 930 output = append(output, fmt.Sprintf("%s%s%s", key, separator, value)) 931 } 932 sort.Strings(output) 933 return output 934 }