github.com/itscaro/cli@v0.0.0-20190705081621-c9db0fe93829/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.Mapping{}): transformMappingOrListFunc("=", false), 308 reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true), 309 reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false), 310 reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false), 311 reflect.TypeOf(types.HostsList{}): transformListOrMappingFunc(":", false), 312 reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig, 313 reflect.TypeOf(types.BuildConfig{}): transformBuildConfig, 314 reflect.TypeOf(types.Duration(0)): transformStringToDuration, 315 } 316 317 for _, transformer := range additionalTransformers { 318 transforms[transformer.TypeOf] = transformer.Func 319 } 320 321 return func(_ reflect.Type, target reflect.Type, data interface{}) (interface{}, error) { 322 transform, ok := transforms[target] 323 if !ok { 324 return data, nil 325 } 326 return transform(data) 327 } 328 } 329 330 // keys needs to be converted to strings for jsonschema 331 func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) { 332 if mapping, ok := value.(map[interface{}]interface{}); ok { 333 dict := make(map[string]interface{}) 334 for key, entry := range mapping { 335 str, ok := key.(string) 336 if !ok { 337 return nil, formatInvalidKeyError(keyPrefix, key) 338 } 339 var newKeyPrefix string 340 if keyPrefix == "" { 341 newKeyPrefix = str 342 } else { 343 newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str) 344 } 345 convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) 346 if err != nil { 347 return nil, err 348 } 349 dict[str] = convertedEntry 350 } 351 return dict, nil 352 } 353 if list, ok := value.([]interface{}); ok { 354 var convertedList []interface{} 355 for index, entry := range list { 356 newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index) 357 convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) 358 if err != nil { 359 return nil, err 360 } 361 convertedList = append(convertedList, convertedEntry) 362 } 363 return convertedList, nil 364 } 365 return value, nil 366 } 367 368 func formatInvalidKeyError(keyPrefix string, key interface{}) error { 369 var location string 370 if keyPrefix == "" { 371 location = "at top level" 372 } else { 373 location = fmt.Sprintf("in %s", keyPrefix) 374 } 375 return errors.Errorf("Non-string key %s: %#v", location, key) 376 } 377 378 // LoadServices produces a ServiceConfig map from a compose file Dict 379 // the servicesDict is not validated if directly used. Use Load() to enable validation 380 func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) { 381 var services []types.ServiceConfig 382 383 for name, serviceDef := range servicesDict { 384 serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, lookupEnv) 385 if err != nil { 386 return nil, err 387 } 388 services = append(services, *serviceConfig) 389 } 390 391 return services, nil 392 } 393 394 // LoadService produces a single ServiceConfig from a compose file Dict 395 // the serviceDict is not validated if directly used. Use Load() to enable validation 396 func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) { 397 serviceConfig := &types.ServiceConfig{} 398 if err := Transform(serviceDict, serviceConfig); err != nil { 399 return nil, err 400 } 401 serviceConfig.Name = name 402 403 if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil { 404 return nil, err 405 } 406 407 if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil { 408 return nil, err 409 } 410 411 serviceConfig.Extras = getExtras(serviceDict) 412 413 return serviceConfig, nil 414 } 415 416 func loadExtras(name string, source map[string]interface{}) map[string]interface{} { 417 if dict, ok := source[name].(map[string]interface{}); ok { 418 return getExtras(dict) 419 } 420 return nil 421 } 422 423 func getExtras(dict map[string]interface{}) map[string]interface{} { 424 extras := map[string]interface{}{} 425 for key, value := range dict { 426 if strings.HasPrefix(key, "x-") { 427 extras[key] = value 428 } 429 } 430 if len(extras) == 0 { 431 return nil 432 } 433 return extras 434 } 435 436 func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv template.Mapping) { 437 for k, v := range vars { 438 interpolatedV, ok := lookupEnv(k) 439 if (v == nil || *v == "") && ok { 440 // lookupEnv is prioritized over vars 441 environment[k] = &interpolatedV 442 } else { 443 environment[k] = v 444 } 445 } 446 } 447 448 func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error { 449 environment := make(map[string]*string) 450 451 if len(serviceConfig.EnvFile) > 0 { 452 var envVars []string 453 454 for _, file := range serviceConfig.EnvFile { 455 filePath := absPath(workingDir, file) 456 fileVars, err := opts.ParseEnvFile(filePath) 457 if err != nil { 458 return err 459 } 460 envVars = append(envVars, fileVars...) 461 } 462 updateEnvironment(environment, 463 opts.ConvertKVStringsToMapWithNil(envVars), lookupEnv) 464 } 465 466 updateEnvironment(environment, serviceConfig.Environment, lookupEnv) 467 serviceConfig.Environment = environment 468 return nil 469 } 470 471 func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error { 472 for i, volume := range volumes { 473 if volume.Type != "bind" { 474 continue 475 } 476 477 if volume.Source == "" { 478 return errors.New(`invalid mount config for type "bind": field Source must not be empty`) 479 } 480 481 filePath := expandUser(volume.Source, lookupEnv) 482 // Check for a Unix absolute path first, to handle a Windows client 483 // with a Unix daemon. This handles a Windows client connecting to a 484 // Unix daemon. Note that this is not required for Docker for Windows 485 // when specifying a local Windows path, because Docker for Windows 486 // translates the Windows path into a valid path within the VM. 487 if !path.IsAbs(filePath) { 488 filePath = absPath(workingDir, filePath) 489 } 490 volume.Source = filePath 491 volumes[i] = volume 492 } 493 return nil 494 } 495 496 // TODO: make this more robust 497 func expandUser(path string, lookupEnv template.Mapping) string { 498 if strings.HasPrefix(path, "~") { 499 home, ok := lookupEnv("HOME") 500 if !ok { 501 logrus.Warn("cannot expand '~', because the environment lacks HOME") 502 return path 503 } 504 return strings.Replace(path, "~", home, 1) 505 } 506 return path 507 } 508 509 func transformUlimits(data interface{}) (interface{}, error) { 510 switch value := data.(type) { 511 case int: 512 return types.UlimitsConfig{Single: value}, nil 513 case map[string]interface{}: 514 ulimit := types.UlimitsConfig{} 515 ulimit.Soft = value["soft"].(int) 516 ulimit.Hard = value["hard"].(int) 517 return ulimit, nil 518 default: 519 return data, errors.Errorf("invalid type %T for ulimits", value) 520 } 521 } 522 523 // LoadNetworks produces a NetworkConfig map from a compose file Dict 524 // the source Dict is not validated if directly used. Use Load() to enable validation 525 func LoadNetworks(source map[string]interface{}, version string) (map[string]types.NetworkConfig, error) { 526 networks := make(map[string]types.NetworkConfig) 527 err := Transform(source, &networks) 528 if err != nil { 529 return networks, err 530 } 531 for name, network := range networks { 532 if !network.External.External { 533 continue 534 } 535 switch { 536 case network.External.Name != "": 537 if network.Name != "" { 538 return nil, errors.Errorf("network %s: network.external.name and network.name conflict; only use network.name", name) 539 } 540 if versions.GreaterThanOrEqualTo(version, "3.5") { 541 logrus.Warnf("network %s: network.external.name is deprecated in favor of network.name", name) 542 } 543 network.Name = network.External.Name 544 network.External.Name = "" 545 case network.Name == "": 546 network.Name = name 547 } 548 network.Extras = loadExtras(name, source) 549 networks[name] = network 550 } 551 return networks, nil 552 } 553 554 func externalVolumeError(volume, key string) error { 555 return errors.Errorf( 556 "conflicting parameters \"external\" and %q specified for volume %q", 557 key, volume) 558 } 559 560 // LoadVolumes produces a VolumeConfig map from a compose file Dict 561 // the source Dict is not validated if directly used. Use Load() to enable validation 562 func LoadVolumes(source map[string]interface{}, version string) (map[string]types.VolumeConfig, error) { 563 volumes := make(map[string]types.VolumeConfig) 564 if err := Transform(source, &volumes); err != nil { 565 return volumes, err 566 } 567 568 for name, volume := range volumes { 569 if !volume.External.External { 570 continue 571 } 572 switch { 573 case volume.Driver != "": 574 return nil, externalVolumeError(name, "driver") 575 case len(volume.DriverOpts) > 0: 576 return nil, externalVolumeError(name, "driver_opts") 577 case len(volume.Labels) > 0: 578 return nil, externalVolumeError(name, "labels") 579 case volume.External.Name != "": 580 if volume.Name != "" { 581 return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name) 582 } 583 if versions.GreaterThanOrEqualTo(version, "3.4") { 584 logrus.Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name) 585 } 586 volume.Name = volume.External.Name 587 volume.External.Name = "" 588 case volume.Name == "": 589 volume.Name = name 590 } 591 volume.Extras = loadExtras(name, source) 592 volumes[name] = volume 593 } 594 return volumes, nil 595 } 596 597 // LoadSecrets produces a SecretConfig map from a compose file Dict 598 // the source Dict is not validated if directly used. Use Load() to enable validation 599 func LoadSecrets(source map[string]interface{}, details types.ConfigDetails) (map[string]types.SecretConfig, error) { 600 secrets := make(map[string]types.SecretConfig) 601 if err := Transform(source, &secrets); err != nil { 602 return secrets, err 603 } 604 for name, secret := range secrets { 605 obj, err := loadFileObjectConfig(name, "secret", types.FileObjectConfig(secret), details) 606 if err != nil { 607 return nil, err 608 } 609 secretConfig := types.SecretConfig(obj) 610 secretConfig.Extras = loadExtras(name, source) 611 secrets[name] = secretConfig 612 } 613 return secrets, nil 614 } 615 616 // LoadConfigObjs produces a ConfigObjConfig map from a compose file Dict 617 // the source Dict is not validated if directly used. Use Load() to enable validation 618 func LoadConfigObjs(source map[string]interface{}, details types.ConfigDetails) (map[string]types.ConfigObjConfig, error) { 619 configs := make(map[string]types.ConfigObjConfig) 620 if err := Transform(source, &configs); err != nil { 621 return configs, err 622 } 623 for name, config := range configs { 624 obj, err := loadFileObjectConfig(name, "config", types.FileObjectConfig(config), details) 625 if err != nil { 626 return nil, err 627 } 628 configConfig := types.ConfigObjConfig(obj) 629 configConfig.Extras = loadExtras(name, source) 630 configs[name] = configConfig 631 } 632 return configs, nil 633 } 634 635 func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfig, details types.ConfigDetails) (types.FileObjectConfig, error) { 636 // if "external: true" 637 switch { 638 case obj.External.External: 639 // handle deprecated external.name 640 if obj.External.Name != "" { 641 if obj.Name != "" { 642 return obj, errors.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name) 643 } 644 if versions.GreaterThanOrEqualTo(details.Version, "3.5") { 645 logrus.Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name) 646 } 647 obj.Name = obj.External.Name 648 obj.External.Name = "" 649 } else { 650 if obj.Name == "" { 651 obj.Name = name 652 } 653 } 654 // if not "external: true" 655 case obj.Driver != "": 656 if obj.File != "" { 657 return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name) 658 } 659 default: 660 obj.File = absPath(details.WorkingDir, obj.File) 661 } 662 663 return obj, nil 664 } 665 666 func absPath(workingDir string, filePath string) string { 667 if filepath.IsAbs(filePath) { 668 return filePath 669 } 670 return filepath.Join(workingDir, filePath) 671 } 672 673 func transformMapStringString(data interface{}) (interface{}, error) { 674 switch value := data.(type) { 675 case map[string]interface{}: 676 return toMapStringString(value, false), nil 677 case map[string]string: 678 return value, nil 679 default: 680 return data, errors.Errorf("invalid type %T for map[string]string", value) 681 } 682 } 683 684 func transformExternal(data interface{}) (interface{}, error) { 685 switch value := data.(type) { 686 case bool: 687 return map[string]interface{}{"external": value}, nil 688 case map[string]interface{}: 689 return map[string]interface{}{"external": true, "name": value["name"]}, nil 690 default: 691 return data, errors.Errorf("invalid type %T for external", value) 692 } 693 } 694 695 func transformServicePort(data interface{}) (interface{}, error) { 696 switch entries := data.(type) { 697 case []interface{}: 698 // We process the list instead of individual items here. 699 // The reason is that one entry might be mapped to multiple ServicePortConfig. 700 // Therefore we take an input of a list and return an output of a list. 701 ports := []interface{}{} 702 for _, entry := range entries { 703 switch value := entry.(type) { 704 case int: 705 v, err := toServicePortConfigs(fmt.Sprint(value)) 706 if err != nil { 707 return data, err 708 } 709 ports = append(ports, v...) 710 case string: 711 v, err := toServicePortConfigs(value) 712 if err != nil { 713 return data, err 714 } 715 ports = append(ports, v...) 716 case map[string]interface{}: 717 ports = append(ports, value) 718 default: 719 return data, errors.Errorf("invalid type %T for port", value) 720 } 721 } 722 return ports, nil 723 default: 724 return data, errors.Errorf("invalid type %T for port", entries) 725 } 726 } 727 728 func transformStringSourceMap(data interface{}) (interface{}, error) { 729 switch value := data.(type) { 730 case string: 731 return map[string]interface{}{"source": value}, nil 732 case map[string]interface{}: 733 return data, nil 734 default: 735 return data, errors.Errorf("invalid type %T for secret", value) 736 } 737 } 738 739 func transformBuildConfig(data interface{}) (interface{}, error) { 740 switch value := data.(type) { 741 case string: 742 return map[string]interface{}{"context": value}, nil 743 case map[string]interface{}: 744 return data, nil 745 default: 746 return data, errors.Errorf("invalid type %T for service build", value) 747 } 748 } 749 750 func transformServiceVolumeConfig(data interface{}) (interface{}, error) { 751 switch value := data.(type) { 752 case string: 753 return ParseVolume(value) 754 case map[string]interface{}: 755 return data, nil 756 default: 757 return data, errors.Errorf("invalid type %T for service volume", value) 758 } 759 } 760 761 func transformServiceNetworkMap(value interface{}) (interface{}, error) { 762 if list, ok := value.([]interface{}); ok { 763 mapValue := map[interface{}]interface{}{} 764 for _, name := range list { 765 mapValue[name] = nil 766 } 767 return mapValue, nil 768 } 769 return value, nil 770 } 771 772 func transformStringOrNumberList(value interface{}) (interface{}, error) { 773 list := value.([]interface{}) 774 result := make([]string, len(list)) 775 for i, item := range list { 776 result[i] = fmt.Sprint(item) 777 } 778 return result, nil 779 } 780 781 func transformStringList(data interface{}) (interface{}, error) { 782 switch value := data.(type) { 783 case string: 784 return []string{value}, nil 785 case []interface{}: 786 return value, nil 787 default: 788 return data, errors.Errorf("invalid type %T for string list", value) 789 } 790 } 791 792 func transformMappingOrListFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) { 793 return func(data interface{}) (interface{}, error) { 794 return transformMappingOrList(data, sep, allowNil), nil 795 } 796 } 797 798 func transformListOrMappingFunc(sep string, allowNil bool) func(interface{}) (interface{}, error) { 799 return func(data interface{}) (interface{}, error) { 800 return transformListOrMapping(data, sep, allowNil), nil 801 } 802 } 803 804 func transformListOrMapping(listOrMapping interface{}, sep string, allowNil bool) interface{} { 805 switch value := listOrMapping.(type) { 806 case map[string]interface{}: 807 return toStringList(value, sep, allowNil) 808 case []interface{}: 809 return listOrMapping 810 } 811 panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping)) 812 } 813 814 func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} { 815 switch value := mappingOrList.(type) { 816 case map[string]interface{}: 817 return toMapStringString(value, allowNil) 818 case ([]interface{}): 819 result := make(map[string]interface{}) 820 for _, value := range value { 821 parts := strings.SplitN(value.(string), sep, 2) 822 key := parts[0] 823 switch { 824 case len(parts) == 1 && allowNil: 825 result[key] = nil 826 case len(parts) == 1 && !allowNil: 827 result[key] = "" 828 default: 829 result[key] = parts[1] 830 } 831 } 832 return result 833 } 834 panic(errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList)) 835 } 836 837 func transformShellCommand(value interface{}) (interface{}, error) { 838 if str, ok := value.(string); ok { 839 return shellwords.Parse(str) 840 } 841 return value, nil 842 } 843 844 func transformHealthCheckTest(data interface{}) (interface{}, error) { 845 switch value := data.(type) { 846 case string: 847 return append([]string{"CMD-SHELL"}, value), nil 848 case []interface{}: 849 return value, nil 850 default: 851 return value, errors.Errorf("invalid type %T for healthcheck.test", value) 852 } 853 } 854 855 func transformSize(value interface{}) (interface{}, error) { 856 switch value := value.(type) { 857 case int: 858 return int64(value), nil 859 case string: 860 return units.RAMInBytes(value) 861 } 862 panic(errors.Errorf("invalid type for size %T", value)) 863 } 864 865 func transformStringToDuration(value interface{}) (interface{}, error) { 866 switch value := value.(type) { 867 case string: 868 d, err := time.ParseDuration(value) 869 if err != nil { 870 return value, err 871 } 872 return types.Duration(d), nil 873 default: 874 return value, errors.Errorf("invalid type %T for duration", value) 875 } 876 } 877 878 func toServicePortConfigs(value string) ([]interface{}, error) { 879 var portConfigs []interface{} 880 881 ports, portBindings, err := nat.ParsePortSpecs([]string{value}) 882 if err != nil { 883 return nil, err 884 } 885 // We need to sort the key of the ports to make sure it is consistent 886 keys := []string{} 887 for port := range ports { 888 keys = append(keys, string(port)) 889 } 890 sort.Strings(keys) 891 892 for _, key := range keys { 893 // Reuse ConvertPortToPortConfig so that it is consistent 894 portConfig, err := opts.ConvertPortToPortConfig(nat.Port(key), portBindings) 895 if err != nil { 896 return nil, err 897 } 898 for _, p := range portConfig { 899 portConfigs = append(portConfigs, types.ServicePortConfig{ 900 Protocol: string(p.Protocol), 901 Target: p.TargetPort, 902 Published: p.PublishedPort, 903 Mode: string(p.PublishMode), 904 }) 905 } 906 } 907 908 return portConfigs, nil 909 } 910 911 func toMapStringString(value map[string]interface{}, allowNil bool) map[string]interface{} { 912 output := make(map[string]interface{}) 913 for key, value := range value { 914 output[key] = toString(value, allowNil) 915 } 916 return output 917 } 918 919 func toString(value interface{}, allowNil bool) interface{} { 920 switch { 921 case value != nil: 922 return fmt.Sprint(value) 923 case allowNil: 924 return nil 925 default: 926 return "" 927 } 928 } 929 930 func toStringList(value map[string]interface{}, separator string, allowNil bool) []string { 931 output := []string{} 932 for key, value := range value { 933 if value == nil && !allowNil { 934 continue 935 } 936 output = append(output, fmt.Sprintf("%s%s%s", key, separator, value)) 937 } 938 sort.Strings(output) 939 return output 940 }