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